view bin/curvecalc @ 637:af5539fe35a7

CC startup doesn't need a subterms file. it'll make the first one Ignore-this: d39d7404a7444c6b937d57257f5bf45f
author Drew Perttula <drewp@bigasterisk.com>
date Thu, 16 Jun 2011 07:11:34 +0000
parents 54c863b2553e
children 7ee76a895427
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, urllib2
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")
        self.timePath = networking.musicPlayer.path("time")
        
    def current_time(self):
        """return deferred which gets called with the current
        time. This gets called really often"""
        d = self.player.request("GET", self.timePath)
        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.musicPlayer.path("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" % self.subterm.submaster.name)
        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)
    print "%s's label is %s" % (subUri, subname)
    if not subname: # fake sub, like for a chase
        st = graph.subjects(L9['sub'], subUri).next()
        subname = graph.label(st)
        print "using parent subterm's name instead. parent %r, name %r" % (st, subname)
    assert subname, "%s has no name" % subUri
    if expr is None:
        expr = '%s(t)' % subname

    term = Subterm(Submaster.Submaster(graph=graph, name=subname, 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:
        assert subterm.submaster.name, "submaster has no name"
        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 = []

    subtermPath = graphPathForSubterms(song)
    try:
        graph.parse(subtermPath, format='n3')
    except urllib2.URLError, e:
        if e.reason.errno != 2:
            raise
        log.info("%s not found, starting with empty graph" % subtermPath)
    
    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("Was Curvemaster 3000MX. Now Curvemaster 30002MX! - %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=600)
        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()