Changeset - f7618f29bb89
[Not reviewed]
default
0 3 0
Drew Perttula - 11 years ago 2014-06-09 03:05:32
drewp@bigasterisk.com
rewrite CurveSet. remove sliders support from curvecalc. curve edits now write quickly to the graph.
Ignore-this: d0338e3a21636d992958594e878962e
3 files changed with 77 insertions and 162 deletions:
0 comments (0 inline, 0 general)
bin/curvecalc
Show inline comments
 
@@ -446,13 +446,13 @@ def launch(args, graph, session, opts, s
 
                          subject=session,
 
                          predicate=L9['currentSong'],
 
                          newObject=song)
 
    except IndexError:
 
        pass
 

	
 
    curveset = Curveset(graph=graph, session=session, sliders=opts.sliders)
 
    curveset = Curveset(graph=graph, session=session)
 
        
 
    log.debug("startup: output %s", time.time() - startTime)
 

	
 
    mt = MaxTime(graph, session)
 
    dispatcher.connect(lambda: mt.get(), "get max time", weak=False)
 

	
 
@@ -481,14 +481,12 @@ def launch(args, graph, session, opts, s
 
    serveCurveEdit(networking.curveCalc.port, hoverTimeResponse, start.curveset)
 

	
 
def main():
 
    startTime = time.time()
 
    parser = optparse.OptionParser()
 
    parser.set_usage("%prog [opts] [songURI]")
 
    parser.add_option("--sliders", action='store_true',
 
                      help='use hardware sliders')
 
    parser.add_option("--debug", action="store_true",
 
                      help="log at DEBUG")
 
    parser.add_option("--reload", action="store_true",
 
                      help="live reload of themes and code")
 
    parser.add_option("--startup-only", action='store_true',
 
                      help="quit after loading everything (for timing tests)")
light9/curvecalc/curve.py
Show inline comments
 
from __future__ import division
 
import glob, time, logging, ast, os
 
from bisect import bisect_left,bisect
 
import louie as dispatcher
 
from twisted.internet import reactor
 
from rdflib import Literal
 
from light9 import showconfig
 
from light9.namespaces import L9, RDF, RDFS
 
from light9.rdfdb.patch import Patch
 

	
 
from bcf2000 import BCF2000
 

	
 
log = logging.getLogger()
 
# todo: move to config, consolidate with ascoltami, musicPad, etc
 
introPad = 4
 
postPad = 4
 

	
 
class Curve(object):
 
@@ -95,12 +94,13 @@ class Curve(object):
 

	
 
    def insert_pt(self, new_pt):
 
        """returns index of new point"""
 
        i = bisect(self.points, (new_pt[0],None))
 
        self.points.insert(i,new_pt)
 
        # missing a check that this isn't the same X as the neighbor point
 
        dispatcher.send("points changed", sender=self)
 
        return i
 

	
 
    def live_input_point(self, new_pt, clear_ahead_secs=.01):
 
        x, y = new_pt
 
        exist = self.points_between(x, x + clear_ahead_secs)
 
        for pt in exist:
 
@@ -110,23 +110,30 @@ class Curve(object):
 
        # now simplify to the left
 
        
 
    def set_points(self, updates):
 
        for i, pt in updates:
 
            self.points[i] = pt
 
            
 
        #self.checkOverlap()
 
        dispatcher.send("points changed", sender=self)
 
            
 
    def checkOverlap(self):
 
        x = None
 
        for p in self.points:
 
            if p[0] <= x:
 
                raise ValueError("overlapping points")
 
            x = p[0]
 

	
 
    def pop_point(self, i):
 
        return self.points.pop(i)
 
        p = self.points.pop(i)
 
        dispatcher.send("points changed", sender=self)
 
        return p
 
            
 
    def remove_point(self, pt):
 
        self.points.remove(pt)
 
        dispatcher.send("points changed", sender=self)
 
            
 
    def indices_between(self, x1, x2, beyond=0):
 
        leftidx = max(0, bisect(self.points, (x1,None)) - beyond)
 
        rightidx = min(len(self.points),
 
                       bisect(self.points, (x2,None)) + beyond)
 
        return range(leftidx, rightidx)
 
@@ -150,44 +157,58 @@ class Curve(object):
 

	
 
class CurveResource(object):
 
    """
 
    holds a Curve, deals with graphs
 
    """
 
    def __init__(self, graph, uri):
 
        # probably newCurve and loadCurve should be the constructors instead.
 
        self.graph, self.uri = graph, uri
 

	
 
    def curvePointsContext(self):
 
        return self.uri
 

	
 
    def newCurve(self, ctx, label):
 
        """
 
        Save type/label for a new :Curve resource.
 
        Pass the ctx where the main curve data (not the points) will go.
 
        """
 
        if hasattr(self, 'curve'):
 
            raise ValueError('CurveResource already has a curve %r' %
 
                             self.curve)
 
        self.graph.patch(Patch(addQuads=[
 
            (self.uri, RDF.type, L9['Curve'], ctx),
 
            (self.uri, RDFS.label, label, ctx),
 
            ]))
 
        self.curve = Curve(self.uri)
 
        self.curve.points.extend([(0, 0)])
 
        self.saveCurve()
 
        self.watchChanges()
 
        
 
    def loadCurve(self):
 
        pts = self.graph.value(self.uri, L9['points'])
 
        if hasattr(self, 'curve'):
 
            raise ValueError('CurveResource already has a curve %r' %
 
                             self.curve)
 
        pointsFile = self.graph.value(self.uri, L9['pointsFile'])
 
        self.curve = Curve(self.uri,
 
                           pointsStorage='file' if pts is None else 'graph')
 
                           pointsStorage='file' if pointsFile else 'graph')
 
        self.graph.addHandler(self.pointsFromGraph)
 
        
 
    def pointsFromGraph(self):
 
        pts = self.graph.value(self.uri, L9['points'])
 
        if pts is not None:
 
            self.curve.set_from_string(pts)
 
        else:
 
            diskPts = self.graph.value(self.uri, L9['pointsFile'])
 
            if diskPts is not None:
 
                self.curve.load(os.path.join(showconfig.curvesDir(), diskPts))
 
            else:
 
                log.warn("curve %s has no points", self.uri)
 
        self.watchCurvePointChanges()
 

	
 
    def saveCurve(self):
 
        self.pendingSave = None
 
        for p in self.getSavePatches():
 
            self.graph.patch(p)
 

	
 
    def getSavePatches(self):
 
        if self.curve.pointsStorage == 'file':
 
            log.warn("not saving file curves anymore- skipping %s" % self.uri)
 
@@ -199,12 +220,30 @@ class CurveResource(object):
 
                subject=self.uri,
 
                predicate=L9['points'],
 
                newObject=Literal(self.curve.points_as_string()))]
 
        else:
 
            raise NotImplementedError(self.curve.pointsStorage)
 

	
 
    def watchCurvePointChanges(self):
 
        """start watching and saving changes to the graph"""
 
        dispatcher.connect(self.onChange, 'points changed', sender=self.curve)
 
        
 
    def onChange(self):
 

	
 
        # Don't write a patch for the edited curve points until they've been
 
        # stable for this long. This can be very short, since it's just to
 
        # stop a 100-point edit from sending many updates. If it's too long,
 
        # you won't see output lights change while you drag a point.  Todo:
 
        # this is just the wrong timing algorithm- it should be a max rate,
 
        # not a max-hold-still-time.
 
        HOLD_POINTS_GRAPH_COMMIT_SECS = .1
 
        
 
        if getattr(self, 'pendingSave', None):
 
            self.pendingSave.cancel()
 
        self.pendingSave = reactor.callLater(HOLD_POINTS_GRAPH_COMMIT_SECS,
 
                                             self.saveCurve)
 
        
 
class Markers(Curve):
 
    """Marker is like a point but the y value is a string"""
 
    def eval(self):
 
        raise NotImplementedError()
 

	
 
@@ -212,88 +251,51 @@ class Markers(Curve):
 
def slope(p1,p2):
 
    if p2[0] == p1[0]:
 
        return 0
 
    return (p2[1] - p1[1]) / (p2[0] - p1[0])
 

	
 

	
 
class Sliders(BCF2000):
 
    def __init__(self, cb, knobCallback, knobButtonCallback):
 
        BCF2000.__init__(self)
 
        self.cb = cb
 
        self.knobCallback = knobCallback
 
        self.knobButtonCallback = knobButtonCallback
 
    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)
 
        if name.startswith("button-knob"):
 
            self.knobButtonCallback(int(name[11:]))
 

	
 
        
 
class Curveset(object):
 
    curves = None # curvename : curve
 
    def __init__(self, graph, session, sliders=False):
 
        """sliders=True means support the hardware sliders"""
 
    def __init__(self, graph, session):
 
        self.graph, self.session = graph, session
 

	
 
        self.currentSong = None
 
        self.curves = {} # name (str) : Curve
 
        self.curveName = {} # reverse
 
        self.curveResources = {} # uri : CurveResource
 
        
 
        self.sliderCurve = {} # slider number (1 based) : curve name
 
        self.sliderNum = {} # reverse
 
        if sliders:
 
            self.sliders = Sliders(self.hw_slider_in, self.hw_knob_in, 
 
                                   self.hw_knob_button)
 
            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
 
        self.markers = Markers(uri=None, pointsStorage='file')
 

	
 
        graph.addHandler(self.loadCurvesForSong)
 

	
 
    def curveFromUri(self, uri):
 
        # self.curves should be indexed by this
 
        for c in self.curves.values():
 
            if c.uri == uri:
 
                return c
 
        raise KeyError("no curve %s" % uri)
 
        return self.curveResources[uri].curve
 

	
 
    def loadCurvesForSong(self):
 
        """
 
        current curves will track song's curves.
 
        
 
        This fires 'add_curve' dispatcher events to announce the new curves.
 
        """
 
        log.info('loadCurvesForSong')
 
        dispatcher.send("clear_curves")
 
        self.curves.clear()
 
        self.curveName.clear()
 
        self.sliderCurve.clear()
 
        self.sliderNum.clear()
 
        self.curveResources.clear()
 
        self.markers = Markers(uri=None, pointsStorage='file')
 
        
 
        self.currentSong = self.graph.value(self.session, L9['currentSong'])
 
        if self.currentSong is None:
 
            return
 

	
 
        for uri in sorted(self.graph.objects(self.currentSong, L9['curve'])):
 
            try:
 
                cr = CurveResource(self.graph, uri)
 
                cr = self.curveResources[uri] = CurveResource(self.graph, uri)
 
                cr.loadCurve()
 

	
 
                curvename = self.graph.label(uri)
 
                if not curvename:
 
                    raise ValueError("curve %r has no label" % uri)
 
                self.add_curve(curvename, cr.curve)
 

	
 
                dispatcher.send("add_curve", sender=self,
 
                                uri=uri, label=curvename, curve=cr.curve)
 
            except Exception as e:
 
                log.error("loading %s failed: %s", uri, e)
 
                
 
        basename = os.path.join(
 
            showconfig.curvesDir(),
 
            showconfig.songFilenameFromURI(self.currentSong))
 
@@ -307,52 +309,35 @@ class Curveset(object):
 
        like basename-curvename, or saves them to the rdf graph"""
 
        basename=os.path.join(
 
            showconfig.curvesDir(),
 
            showconfig.songFilenameFromURI(self.currentSong))
 

	
 
        patches = []
 
        for label, curve in self.curves.items():
 
            cr = CurveResource(self.graph, curve.uri)
 
            cr.curve = curve
 
        for cr in self.curveResources.values():
 
            patches.extend(cr.getSavePatches())
 

	
 
        self.markers.save("%s.markers" % basename)
 
        # this will cause reloads that will clear our curve list
 
        # this will cause reloads that will rebuild our curve list
 
        for p in patches:
 
            self.graph.patch(p)
 
            
 
    def sorter(self, name):
 
        return self.curves[name].uri
 
        
 
    def curveNamesInOrder(self):
 
        return sorted(self.curves.keys(), key=self.sorter)
 
            
 
    def add_curve(self, name, curve):
 
        # should be indexing by uri
 
        if isinstance(name, Literal):
 
            name = str(name) 
 
        if name in self.curves:
 
            raise ValueError("can't add a second curve named %r" % name)
 
        self.curves[name] = curve
 
        self.curveName[curve] = name
 
    def curveUrisInOrder(self):
 
        return sorted(self.curveResources.keys())
 

	
 
        if self.sliders and name not in ['smooth_music', 'music']:
 
            num = len(self.sliderCurve) + 1
 
            if num <= 8:
 
                self.sliderCurve[num] = name
 
                self.sliderNum[name] = num
 
            else:
 
                num = None
 
        else:
 
            num = None
 
            
 
        dispatcher.send("add_curve", slider=num, knobEnabled=num is not None,
 
                        sender=self, name=name)
 
    def currentCurves(self):
 
        # deprecated
 
        for uri, cr in sorted(self.curveResources.items()):
 
            with self.graph.currentState(
 
                    tripleFilter=(uri, RDFS['label'], None)) as g:
 
                yield uri, g.label(uri), cr.curve
 

	
 
    def globalsdict(self):
 
        return self.curves.copy()
 
        raise NotImplementedError('subterm used to get a dict of name:curve')
 
    
 
    def get_time_range(self):
 
        return 0, dispatcher.send("get max time")[0][1]
 

	
 
    def new_curve(self, name, renameIfExisting=True):
 
        if isinstance(name, Literal):
 
@@ -364,69 +349,17 @@ class Curveset(object):
 
            return
 
        while name in self.curves:
 
           name=name+"-1"
 

	
 
        uri = self.graph.sequentialUri(self.currentSong + '/curve-')
 

	
 
        cr = CurveResource(self.graph, uri)
 
        cr = self.curveResources[uri] = CurveResource(self.graph, uri)
 
        cr.newCurve(ctx=self.currentSong, label=Literal(name))
 
        s, e = self.get_time_range()
 
        cr.curve.points.extend([(s, 0), (e, 0)])
 

	
 
        ctx = self.currentSong
 
        self.graph.patch(Patch(addQuads=[
 
            (self.currentSong, L9['curve'], uri, ctx),
 
            ]))
 
        cr.saveCurve()
 

	
 
    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("set key", 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 hw_knob_button(self, num):
 
        try:
 
            curve = self.curves[self.sliderCurve[num]]
 
        except KeyError:
 
            return
 

	
 
        dispatcher.send("set key", curve=curve)
 
        
 

	
 
    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)
light9/curvecalc/curveview.py
Show inline comments
 
@@ -523,25 +523,23 @@ class Curveview(object):
 
        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.set_points([(idx, (pos[0], value))])
 
            self.update_curve()
 

	
 
    def slider_in(self, curve, value=None):
 
        """user pushed on a slider. make a new key.  if value is None,
 
        the value will be the same as the last."""
 
        if curve != self.curve:
 
            return
 

	
 
        if value is None:
 
            value = self.curve.eval(self.current_time())
 

	
 
        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" % (
 
                msg, self.dragging_dots, self.selecting)
 

	
 
@@ -596,13 +594,12 @@ class Curveview(object):
 
            if nextStatic <= len(cp) - 1:
 
                maxRight = min(maxRight, cp[nextStatic][0] - cp[i][0])
 
        return maxLeft, maxRight
 

	
 
    def setPoints(self, updates):
 
        self.curve.set_points(updates)
 
        self.update_curve()
 
        
 
    def selectionChanged(self):
 
        if self.selectManip:
 
            self.selectManip.update()
 

	
 
    def select_press(self,ev):
 
@@ -927,21 +924,19 @@ class Curveview(object):
 
        x = p[0]
 
        y = self.curve.eval(x)
 
        self.add_point((x, y))
 

	
 
    def add_points(self, pts):
 
        idxs = [self.curve.insert_pt(p) for p in pts]
 
        self.update_curve()
 
        self.select_indices(idxs)
 
        
 
    def add_point(self, p):
 
        self.add_points([p])
 

	
 
    def add_marker(self, p):
 
        self.markers.insert_pt(p)
 
        self.update_curve()
 
        
 
    def remove_point_idx(self, *idxs):
 
        idxs = list(idxs)
 
        while idxs:
 
            i = idxs.pop()
 

	
 
@@ -960,14 +955,12 @@ class Curveview(object):
 
                    ii -= 1
 
                newidxs.append(idxs[ii])
 

	
 
            self.select_indices(newsel)
 
            idxs[:] = newidxs
 
            
 
        self.update_curve()
 

	
 
    def highlight_selected_dots(self):
 
        if not self.redrawsEnabled:
 
            return
 

	
 
        for i,d in self.dots.items():
 
            if i in self.selected_points:
 
@@ -1017,16 +1010,14 @@ class Curveview(object):
 
            delta = (cur[0] - self.last_mouse_world[0],
 
                     cur[1] - self.last_mouse_world[1])
 
        else:
 
            delta = 0,0
 
        self.last_mouse_world = cur
 

	
 
        moved = self.translate_points(delta)
 
        self.translate_points(delta)
 
        
 
        if moved:
 
            self.update_curve()
 

	
 
    def translate_points(self, delta):
 
        moved = False
 
        
 
        cp = self.curve.points
 
        updates = []
 
@@ -1076,28 +1067,28 @@ class CurveRow(object):
 
    one of the repeating curve rows (including widgets on the left)
 

	
 
    i wish these were in a list-style TreeView so i could set_reorderable on it
 

	
 
    please pack self.box
 
    """
 
    def __init__(self, graph, name, curve, markers, slider, knobEnabled, zoomControl):
 
    def __init__(self, graph, name, curve, markers, zoomControl):
 
        self.graph = graph
 
        self.name = name
 
        self.box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
 
        self.box.set_border_width(1)
 

	
 
        self.cols = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
 
        self.box.add(self.cols)
 
        
 
        controls = Gtk.Frame()
 
        controls.set_size_request(160, -1)
 
        controls.set_shadow_type(Gtk.ShadowType.OUT)
 
        self.cols.pack_start(controls, expand=False, fill=True, padding=0)
 
        self.setupControls(controls, name, curve, slider)
 
        self.setupControls(controls, name, curve)
 

	
 
        self.curveView = Curveview(curve, markers, knobEnabled=knobEnabled,
 
        self.curveView = Curveview(curve, markers,
 
                                   isMusic=name in ['music', 'smooth_music'],
 
                                   zoomControl=zoomControl)
 
        
 
        self.initCurveView()
 
        dispatcher.connect(self.rebuild, "all curves rebuild")
 

	
 
@@ -1122,13 +1113,13 @@ class CurveRow(object):
 

	
 
    def setHeight(self, h):
 
        self.curveView.widget.set_size_request(-1, h)
 
        # the event watcher wasn't catching these
 
        reactor.callLater(.5, self.curveView.update_curve)
 
        
 
    def setupControls(self, controls, name, curve, slider):
 
    def setupControls(self, controls, name, curve):
 
        box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
 
        controls.add(box)
 

	
 
        curve_name_label = Gtk.LinkButton()
 
        def update_label():
 
            # todo: abort if we don't still exist...
 
@@ -1139,19 +1130,12 @@ class CurveRow(object):
 
        
 

	
 
        self.muted = Gtk.CheckButton("M")
 
        self.muted.connect("toggled", self.sync_mute_to_curve)
 
        dispatcher.connect(self.mute_changed, 'mute changed', sender=curve)
 

	
 
        self.sliderLabel = None
 
        if slider is not None:
 
            # slider should have a checkbutton, defaults to off for
 
            # music tracks
 
            self.sliderLabel = Gtk.Label("Slider %s" % slider)
 
            box.pack_start(self.sliderLabel, expand=True, fill=True, padding=0)
 

	
 
        box.pack_start(curve_name_label, expand=True, fill=True, padding=0)
 
        box.pack_start(self.muted, expand=True, fill=True, padding=0)
 

	
 
    def onDelete(self):
 
        self.curveView.onDelete()
 
        
 
@@ -1190,14 +1174,14 @@ class Curvesetview(object):
 
        self.allCurveRows = set()
 
        self.visibleHeight = 1000
 

	
 
        self.zoomControl = self.initZoomControl(zoomControlBox)
 
        self.zoomControl.redrawzoom()
 
        
 
        for c in curveset.curveNamesInOrder():
 
            self.add_curve(c) 
 
        for uri, label, curve in curveset.currentCurves():
 
            self.add_curve(uri, label, curve) 
 

	
 
        dispatcher.connect(self.clear_curves, "clear_curves")
 
        dispatcher.connect(self.add_curve, "add_curve", sender=self.curveset)
 
        dispatcher.connect(self.set_featured_curves, "set_featured_curves")
 
        dispatcher.connect(self.song_has_changed, "song_has_changed")
 
        
 
@@ -1267,18 +1251,18 @@ class Curvesetview(object):
 
        self.entry.focus()
 

	
 
    def new_curve(self, event):
 
        self.curveset.new_curve(self.newcurvename.get())
 
        self.newcurvename.set('')
 
        
 
    def add_curve(self, name, slider=None, knobEnabled=False):
 
        if isinstance(name, Literal):
 
            name = str(name)
 
        curve = self.curveset.curves[name]
 
        f = CurveRow(self.graph, name, curve, self.curveset.markers,
 
                     slider, knobEnabled, self.zoomControl)
 
    def add_curve(self, uri, label, curve):
 
        if isinstance(label, Literal):
 
            label = str(label)
 

	
 
        f = CurveRow(self.graph, label, curve, self.curveset.markers,
 
                     self.zoomControl)
 
        self.curvesVBox.pack_start(f.box, expand=True, fill=True, padding=0)
 
        f.box.show_all()
 
        self.allCurveRows.add(f)
 
        self.setRowHeights()
 
        f.curveView.goLive()
 

	
0 comments (0 inline, 0 general)