view bin/curvecalc @ 517:f15ffbfc5cd6

speed up curvecalc tk update to 10ms (since we recently sped up the recalcs) Ignore-this: 6acdb9b5aab226565d61116c93118b12
author drewp@bigasterisk.com
date Mon, 29 Jun 2009 04:08:43 +0000
parents 73b181155555
children f2dbb0b1fb35
line wrap: on
line source

#!/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 twisted
from twisted.web.xmlrpc import Proxy
from rdflib import Literal, URIRef, RDF, RDFS
from rdflib.Graph import Graph
import rdflib
import logging
log = logging.getLogger()
logging.basicConfig(format="%(asctime)s %(levelname)-5s %(name)s %(filename)s:%(lineno)d: %(message)s")
log.setLevel(logging.DEBUG)

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 Music:
    def __init__(self):
        self.player=None # xmlrpc Proxy to player
        self.recenttime=0

        dispatcher.connect(self.seekplay_or_pause,"music seek")
        
    def current_time(self):
        """return deferred which gets called with the current time"""
        if self.player is None:
            print "connect to player"
            self.player = Proxy(networking.musicUrl())
#            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
    
    def seekplay_or_pause(self,t):
        self.player.callRemote('seekplay_or_pause',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 "<Subterm %s %s>" % (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" % graph.label(self.subterm.submaster.uri))
        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)
    if expr is None:
        expr = '%s(t)' % subname

    term = Subterm(Submaster.Submaster(graph=graph, 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("<Key-Return>", 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:
        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']):
        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("<Control-Key-s>",
              lambda *args: savekey(song, subterms, curveset))
    root.bind("<Control-Key-r>", lambda evt: dispatcher.send('reload all subs'))
    root.bind("<Control-Key-n>",
              lambda evt: dispatcher.send('focus new subterm'))
    root.bind("<Control-Key-N>", lambda evt: dispatcher.send('focus new curve'))
    root.bind("<Control-Key-q>",lambda ev: reactor.stop)
    root.bind("<Destroy>",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 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()

    try:
        song = URIRef(args[0])
    except IndexError:
        raise SystemExit("song URI is required, e.g. 'http://light9.bigasterisk.com/show/dance2008/song3'")

    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("Curvemaster 3000MX - %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)

        csv = Curvesetview(panes.subwidget('curvesetView'), curveset,
                           height=400)
        csv.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")

    tksupport.install(root, ms=10)
    log.debug("run")
    prof.run(reactor.run, profile=False)

main()