view bin/curvecalc @ 590:0cf00fffd921

logging Ignore-this: a30c74ea5c5d2e0ca9ecb8ced1f7ef47
author drewp@bigasterisk.com
date Sun, 20 Jun 2010 05:04:09 +0000
parents ad5043f70fda
children b50e4d43dd3e
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 jsonlib, restkit
import twisted
from twisted.web.client import Agent
from twisted.internet.protocol import Protocol
from twisted.internet.defer import Deferred     
from zope.interface import implements
from twisted.internet.defer import succeed
from twisted.web.iweb import IBodyProducer
from rdflib import Literal, URIRef, RDF, RDFS
from rdflib.Graph import Graph
import rdflib
import logging
log = logging.getLogger()

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 GatherJson(Protocol):
    """calls back the 'finished' deferred with the parsed json data we
    received"""
    def __init__(self, finished):
        self.finished = finished
        self.buf = ""

    def dataReceived(self, bytes):
        self.buf += bytes

    def connectionLost(self, reason):
        self.finished.callback(jsonlib.read(self.buf, use_float=True))

class StringProducer(object):
    # http://twistedmatrix.com/documents/current/web/howto/client.html
    implements(IBodyProducer)

    def __init__(self, body):
        self.body = body
        self.length = len(body)

    def startProducing(self, consumer):
        consumer.write(self.body)
        return succeed(None)

    def pauseProducing(self):
        pass

    def stopProducing(self):
        pass

class Music:
    def __init__(self):
        self.recenttime=0
        self.player = Agent(reactor)
        dispatcher.connect(self.seekplay_or_pause,"music seek")
        
    def current_time(self):
        """return deferred which gets called with the current time"""
        d = self.player.request("GET", networking.musicUrl() + "time")
        d.addCallback(self._timeReturned)
        return d

    def _timeReturned(self, response):
        done = Deferred()
        done.addCallback(self._bodyReceived)
        response.deliverBody(GatherJson(done))
        return done

    def _bodyReceived(self, data):
        dispatcher.send("input time",val=data['t'])
        return data['t'] # pass along to the real receiver
    
    def seekplay_or_pause(self,t):
        d = self.player.request("POST",
                                networking.musicUrl() + "seekPlayOrPause", bodyProducer=StringProducer(jsonlib.write({"t" : 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']):
        log.info("song %s has subterm %s", song, st)
        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 currentlyPlayingSong():
    """ask the music player what song it's on"""
    t = jsonlib.read(restkit.Resource(networking.musicUrl()).get("time").body)
    if t['song'] is None:
        raise ValueError("music player is not playing any song")
    return URIRef(t['song'])

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

    logging.basicConfig(format="%(asctime)s %(levelname)-5s %(name)s %(filename)s:%(lineno)d: %(message)s")
    log.setLevel(logging.INFO)


    try:
        song = URIRef(args[0])
    except IndexError:
        song = currentlyPlayingSong()

    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)

        curvesetView = Curvesetview(panes.subwidget('curvesetView'), curveset,
                                    height=400)
        curvesetView.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")
    
    # this is scheduled after some tk shuffling, to try to minimize
    # the number of times we redraw the curve at startup. If tk is
    # very slow, it's ok. You'll just get some wasted redraws.
    reactor.callLater(.1, curvesetView.goLive)

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

main()