Changeset - 1aa91a31c0e2
[Not reviewed]
default
0 13 0
Drew Perttula - 6 years ago 2019-05-25 12:06:01
drewp@bigasterisk.com
reformat some missed files
Ignore-this: f13152975437adeb48ed619ab676365e
12 files changed with 147 insertions and 53 deletions:
0 comments (0 inline, 0 general)
light9/Effects.py
Show inline comments
 

	
 
import random as random_mod
 
import math
 
import logging, colorsys
 
import light9.Submaster as Submaster
 
from .chase import chase as chase_logic
 
from . import showconfig
 
from rdflib import RDF
 
from light9 import Patch
 
from light9.namespaces import L9
 
log = logging.getLogger()
 

	
 
registered = []
 

	
 

	
 
def register(f):
 
    registered.append(f)
 
    return f
 

	
 

	
 
@register
 
class Strip(object):
 
    """list of r,g,b tuples for sending to an LED strip"""
 
    which = 'L'  # LR means both. W is the wide one
 
    pixels = []
light9/FlyingFader.py
Show inline comments
 
from tkinter.tix import *
 
from time import time, sleep
 

	
 

	
 

	
 
class Mass:
 

	
 
    def __init__(self):
 
        self.x = 0  # position
 
        self.xgoal = 0  # position goal
 

	
 
        self.v = 0  # velocity
 
        self.maxspeed = .8  # maximum speed, in position/second
 
        self.maxaccel = 3  # maximum acceleration, in position/second^2
 
        self.eps = .03  # epsilon - numbers within this much are considered the same
 

	
 
        self._lastupdate = time()
 
        self._stopped = 1
 

	
 
    def equal(self, a, b):
 
        return abs(a - b) < self.eps
 

	
 
    def stop(self):
 
        self.v = 0
 
        self.xgoal = self.x
 
        self._stopped = 1
 

	
 
    def update(self):
 
        t0 = self._lastupdate
light9/Submaster.py
Show inline comments
 

	
 
import os, logging, time
 
from rdflib import Graph, RDF
 
from rdflib import RDFS, Literal, BNode
 
from light9.namespaces import L9, XSD
 
from light9.TLUtility import dict_scale, dict_max
 
from light9 import showconfig
 
from light9.Patch import resolve_name, get_dmx_channel, get_channel_uri, reload_data
 
from louie import dispatcher
 
from .rdfdb.patch import Patch
 
log = logging.getLogger('submaster')
 

	
 

	
 
class Submaster(object):
 
    """mapping of channels to levels"""
 

	
 
    def __init__(self, name, levels):
 
        """this sub has a name just for debugging. It doesn't get persisted.
 
        See PersistentSubmaster.
 

	
 
        levels is a dict
 
        """
 
        self.name = name
 
        self.levels = levels
 

	
 
        self.temporary = True
 

	
 
        if not self.temporary:
 
            # obsolete
 
            dispatcher.connect(log.error, 'reload all subs')
 

	
 
        #log.debug("%s initial levels %s", self.name, self.levels)
 

	
 
    def _editedLevels(self):
 
        pass
 

	
 
    def set_level(self, channelname, level, save=True):
 
        self.levels[resolve_name(channelname)] = level
 
        self._editedLevels()
 

	
 
    def set_all_levels(self, leveldict):
 
        self.levels.clear()
 
        for k, v in list(leveldict.items()):
 
            # this may call _editedLevels too many times
 
            self.set_level(k, v, save=0)
 

	
 
    def get_levels(self):
 
        return self.levels
 

	
 
    def no_nonzero(self):
 
        return all(v == 0 for v in self.levels.values())
 

	
 
    def __mul__(self, scalar):
 
        return Submaster("%s*%s" % (self.name, scalar),
 
                         levels=dict_scale(self.levels, scalar))
 

	
 
    __rmul__ = __mul__
 

	
 
    def max(self, *othersubs):
 
        return sub_maxes(self, *othersubs)
 

	
 
    def __add__(self, other):
 
        return self.max(other)
 

	
 
    def ident(self):
 
        return (self.name, tuple(sorted(self.levels.items())))
 

	
 
    def __repr__(self):
 
        items = sorted(list(getattr(self, 'levels', {}).items()))
 
        levels = ' '.join(["%s:%.2f" % item for item in items])
 
        return "<'%s': [%s]>" % (getattr(self, 'name', 'no name yet'), levels)
 

	
 
    def __cmp__(self, other):
 
        # not sure how useful this is
 
        if not isinstance(other, Submaster):
 
            return -1
 
        return cmp(self.ident(), other.ident())
 

	
 
    def __hash__(self):
 
        return hash(self.ident())
 

	
 
    def get_dmx_list(self):
 
        leveldict = self.get_levels() # gets levels of sub contents
 

	
 
        levels = []
 
        for k, v in list(leveldict.items()):
 
            if v == 0:
 
                continue
 
            try:
 
                dmxchan = get_dmx_channel(k) - 1
 
            except ValueError:
 
                log.error("error trying to compute dmx levels for submaster %s"
 
                          % self.name)
 
                log.error(
 
                    "error trying to compute dmx levels for submaster %s" %
 
                    self.name)
 
                raise
 
            if dmxchan >= len(levels):
 
                levels.extend([0] * (dmxchan - len(levels) + 1))
 
            levels[dmxchan] = max(v, levels[dmxchan])
 

	
 
        return levels
 

	
 
    def normalize_patch_names(self):
 
        """Use only the primary patch names."""
 
        # possibly busted -- don't use unless you know what you're doing
 
        self.set_all_levels(self.levels.copy())
 

	
 
    def get_normalized_copy(self):
 
        """Get a copy of this sumbaster that only uses the primary patch
 
        names.  The levels will be the same."""
 
        newsub = Submaster("%s (normalized)" % self.name, {})
 
        newsub.set_all_levels(self.levels)
 
        return newsub
 

	
 
    def crossfade(self, othersub, amount):
 
        """Returns a new sub that is a crossfade between this sub and
 
        another submaster.
 

	
 
        NOTE: You should only crossfade between normalized submasters."""
 
        otherlevels = othersub.get_levels()
 
        keys_set = {}
 
        for k in list(self.levels.keys()) + list(otherlevels.keys()):
 
            keys_set[k] = 1
 
        all_keys = list(keys_set.keys())
 

	
 
        xfaded_sub = Submaster("xfade", {})
 
        for k in all_keys:
 
            xfaded_sub.set_level(k,
 
                                 linear_fade(self.levels.get(k, 0),
 
                                             otherlevels.get(k, 0),
 
            xfaded_sub.set_level(
 
                k,
 
                linear_fade(self.levels.get(k, 0), otherlevels.get(k, 0),
 
                                             amount))
 

	
 
        return xfaded_sub
 

	
 

	
 
class PersistentSubmaster(Submaster):
 

	
 
    def __init__(self, graph, uri):
 
        if uri is None:
 
            raise TypeError("uri must be URIRef")
 
        self.graph, self.uri = graph, uri
 
        self.graph.addHandler(self.setName)
 
        self.graph.addHandler(self.setLevels)
 
        Submaster.__init__(self, self.name, self.levels)
 
        self.temporary = False
 

	
 
    def ident(self):
 
        return self.uri
 

	
 
    def _editedLevels(self):
 
        self.save()
 

	
 
    def changeName(self, newName):
 
        self.graph.patchObject(self.uri, self.uri, RDFS.label, Literal(newName))
 
        
 
    def setName(self):
 
        log.info("sub update name %s %s", self.uri, self.graph.label(self.uri))
 
        self.name = self.graph.label(self.uri)
 

	
 
    def setLevels(self):
 
        log.debug("sub update levels")
 
@@ -172,201 +178,211 @@ class PersistentSubmaster(Submaster):
 
                # broken lightLevel link- may be from someone deleting channels
 
                log.warn("sub %r has lightLevel %r with channel %r "
 
                         "and level %r" % (self.uri, lev, chan, val))
 
                continue
 
            log.debug("   new val %r", val)
 
            if val == 0:
 
                continue
 
            name = self.graph.label(chan)
 
            if not name:
 
                log.error("sub %r has channel %r with no name- "
 
                          "leaving out that channel" % (self.name, chan))
 
                continue
 
            try:
 
                self.levels[name] = float(val)
 
            except:
 
                log.error("name %r val %r" % (name, val))
 
                raise
 

	
 
    def _saveContext(self):
 
        """the context in which we should save all the lightLevel triples for
 
        this sub"""
 
        typeStmt = (self.uri, RDF.type, L9['Submaster'])
 
        with self.graph.currentState(tripleFilter=typeStmt) as current:
 
            try:
 
                log.debug("submaster's type statement is in %r so we save there" %
 
                log.debug(
 
                    "submaster's type statement is in %r so we save there" %
 
                          list(current.contextsForStatement(typeStmt)))
 
                ctx = current.contextsForStatement(typeStmt)[0]
 
            except IndexError:
 
                log.info("declaring %s to be a submaster" % self.uri)
 
                ctx = self.uri
 
                self.graph.patch(Patch(addQuads=[
 
                self.graph.patch(
 
                    Patch(addQuads=[
 
                    (self.uri, RDF.type, L9['Submaster'], ctx),
 
                    ]))
 

	
 
        return ctx
 

	
 
    def editLevel(self, chan, newLevel):
 
        self.graph.patchMapping(self._saveContext(),
 
                                subject=self.uri, predicate=L9['lightLevel'],
 
                                subject=self.uri,
 
                                predicate=L9['lightLevel'],
 
                                nodeClass=L9['ChannelSetting'],
 
                                keyPred=L9['channel'], newKey=chan,
 
                                keyPred=L9['channel'],
 
                                newKey=chan,
 
                                valuePred=L9['level'],
 
                                newValue=Literal(newLevel))
 

	
 
    def clear(self):
 
        """set all levels to 0"""
 
        with self.graph.currentState() as g:
 
            levs = list(g.objects(self.uri, L9['lightLevel']))
 
        for lev in levs:
 
            self.graph.removeMappingNode(self._saveContext(), lev)
 

	
 
    def allQuads(self):
 
        """all the quads for this sub"""
 
        quads = []
 
        with self.graph.currentState() as current:
 
            quads.extend(current.quads((self.uri, None, None)))
 
            for s,p,o,c in quads:
 
                if p == L9['lightLevel']:
 
                    quads.extend(current.quads((o, None, None)))
 
        return quads
 

	
 

	
 
    def save(self):
 
        raise NotImplementedError("obsolete?")
 
        if self.temporary:
 
            log.info("not saving temporary sub named %s",self.name)
 
            return
 

	
 
        graph = Graph()
 
        subUri = L9['sub/%s' % self.name]
 
        graph.add((subUri, RDFS.label, Literal(self.name)))
 
        for chan in list(self.levels.keys()):
 
            try:
 
                chanUri = get_channel_uri(chan)
 
            except KeyError:
 
                log.error("saving dmx channels with no :Channel node "
 
                          "is not supported yet. Give channel %s a URI "
 
                          "for it to be saved. Omitting this channel "
 
                          "from the sub." % chan)
 
                continue
 
            lev = BNode()
 
            graph.add((subUri, L9['lightLevel'], lev))
 
            graph.add((lev, L9['channel'], chanUri))
 
            graph.add((lev, L9['level'],
 
                       Literal(self.levels[chan], datatype=XSD['decimal'])))
 

	
 
        graph.serialize(showconfig.subFile(self.name), format="nt")
 

	
 

	
 
def linear_fade(start, end, amount):
 
    """Fades between two floats by an amount.  amount is a float between
 
    0 and 1.  If amount is 0, it will return the start value.  If it is 1,
 
    the end value will be returned."""
 
    level = start + (amount * (end - start))
 
    return level
 

	
 

	
 
def sub_maxes(*subs):
 
    nonzero_subs = [s for s in subs if not s.no_nonzero()]
 
    name = "max(%s)" % ", ".join([repr(s) for s in nonzero_subs])
 
    return Submaster(name,
 
                     levels=dict_max(*[sub.levels for sub in nonzero_subs]))
 

	
 

	
 
def combine_subdict(subdict, name=None, permanent=False):
 
    """A subdict is { Submaster objects : levels }.  We combine all
 
    submasters first by multiplying the submasters by their corresponding
 
    levels and then max()ing them together.  Returns a new Submaster
 
    object.  You can give it a better name than the computed one that it
 
    will get or make it permanent if you'd like it to be saved to disk.
 
    Serves 8."""
 
    scaledsubs = [sub * level for sub, level in list(subdict.items())]
 
    maxes = sub_maxes(*scaledsubs)
 
    if name:
 
        maxes.name = name
 
    if permanent:
 
        maxes.temporary = False
 

	
 
    return maxes
 

	
 

	
 
class Submasters(object):
 
    "Collection o' Submaster objects"
 

	
 
    def __init__(self, graph):
 
        self.submasters = {} # uri : Submaster
 
        self.graph = graph
 

	
 
        graph.addHandler(self.findSubs)
 

	
 
    def findSubs(self):
 
        current = set()
 

	
 
        for s in self.graph.subjects(RDF.type, L9['Submaster']):
 
            if self.graph.contains((s, RDF.type, L9['LocalSubmaster'])):
 
                continue
 
            log.debug("found sub %s", s)
 
            if s not in self.submasters:
 
                sub = self.submasters[s] = PersistentSubmaster(self.graph, s)
 
                dispatcher.send("new submaster", sub=sub)
 
            current.add(s)
 
        for s in set(self.submasters.keys()) - current:
 
            del self.submasters[s]
 
            dispatcher.send("lost submaster", subUri=s)
 
        log.info("findSubs finished, %s subs", len(self.submasters))
 

	
 
    def get_all_subs(self):
 
        "All Submaster objects"
 
        l = sorted(list(self.submasters.items()))
 
        l = [x[1] for x in l]
 
        songs = []
 
        notsongs = []
 
        for s in l:
 
            if s.name and s.name.startswith('song'):
 
                songs.append(s)
 
            else:
 
                notsongs.append(s)
 
        combined = notsongs + songs
 

	
 
        return combined
 

	
 
    def get_sub_by_uri(self, uri):
 
        return self.submasters[uri]
 

	
 
    def get_sub_by_name(self, name):
 
        return get_sub_by_name(name, self)
 

	
 

	
 
# a global instance of Submasters, created on demand
 
_submasters = None
 

	
 

	
 
def get_global_submasters(graph):
 
    """
 
    Get (and make on demand) the global instance of
 
    Submasters. Cached, but the cache is not correctly using the graph
 
    argument. The first graph you pass will stick in the cache.
 
    """
 
    global _submasters
 
    if _submasters is None:
 
        _submasters = Submasters(graph)
 
    return _submasters
 

	
 

	
 
def get_sub_by_name(name, submasters=None):
 
    """name is a channel or sub nama, submasters is a Submasters object.
 
    If you leave submasters empty, it will use the global instance of
 
    Submasters."""
 
    if not submasters:
 
        submasters = get_global_submasters()
 

	
 
    # get_all_sub_names went missing. needs rework
 
    #if name in submasters.get_all_sub_names():
 
    #    return submasters.get_sub_by_name(name)
 

	
 
    try:
 
        val = int(name)
 
        s = Submaster("#%d" % val, levels={val : 1.0})
 
        return s
 
    except ValueError:
 
        pass
 

	
 
    try:
 
        subnum = get_dmx_channel(name)
 
        s = Submaster("'%s'" % name, levels={subnum : 1.0})
 
        return s
 
    except ValueError:
 
        pass
light9/TLUtility.py
Show inline comments
 
"""Collected utility functions, many are taken from Drew's utils.py in
 
Cuisine CVS and Hiss's Utility.py."""
 

	
 

	
 
import sys
 

	
 
__author__ = "David McClosky <dmcc@bigasterisk.com>, " + \
 
             "Drew Perttula <drewp@bigasterisk.com>"
 
__cvsid__ = "$Id: TLUtility.py,v 1.1 2003/05/25 08:25:35 dmcc Exp $"
 
__version__ = "$Revision: 1.1 $"[11:-2]
 

	
 

	
 
def make_attributes_from_args(*argnames):
 
    """
 
    This function simulates the effect of running
 
      self.foo=foo
 
    for each of the given argument names ('foo' in the example just
 
    now). Now you can write:
 
        def __init__(self,foo,bar,baz):
 
            copy_to_attributes('foo','bar','baz')
 
            ...
 
    instead of:
 
        def __init__(self,foo,bar,baz):
 
            self.foo=foo
 
            self.bar=bar
 
            self.baz=baz
 
            ... 
 
    """
 
    
 
    callerlocals=sys._getframe(1).f_locals
 
    callerself=callerlocals['self']
 
    for a in argnames:
 
        try:
 
            setattr(callerself,a,callerlocals[a])
 
        except KeyError:
 
            raise KeyError("Function has no argument '%s'" % a)
 

	
 

	
 
def enumerate(*collections):
 
    """Generates an indexed series:  (0,coll[0]), (1,coll[1]) ...
 
    
 
    this is a multi-list version of the code from the PEP:
 
    enumerate(a,b) gives (0,a[0],b[0]), (1,a[1],b[1]) ...
 
    """
 
    i = 0
 
    iters = [iter(collection) for collection in collections]
 
    while True:
 
        yield [i,] + [next(iterator) for iterator in iters]
 
        yield [
 
            i,
 
        ] + [next(iterator) for iterator in iters]
 
        i += 1
 

	
 

	
 
def dumpobj(o):
 
    """Prints all the object's non-callable attributes"""
 
    print(repr(o))
 
    for a in [x for x in dir(o) if not callable(getattr(o, x))]:
 
        try:
 
            print("  %20s: %s " % (a, getattr(o, a)))
 
        except:
 
            pass
 
    print("")
 

	
 

	
 
def dict_filter_update(d, **newitems):
 
    """Adds a set of new keys and values to dictionary 'd' if the values are
 
    true:
 

	
 
    >>> some_dict = {}
 
    >>> dict_filter_update(some_dict, a=None, b=0, c=1, e={}, s='hello')
 
    >>> some_dict
 
    {'c': 1, 's': 'hello'}
 
    """
 
    for k, v in list(newitems.items()):
 
        if v: d[k] = v
 

	
 

	
 
def try_get_logger(channel):
 
    """Tries to get a logger with the channel 'channel'.  Will return a
 
    silent DummyClass if logging is not available."""
 
    try:
 
        import logging
 
        log = logging.getLogger(channel)
 
    except ImportError:
 
        log = DummyClass()
 
    return log
 

	
 

	
 
class DummyClass:
 
    """A class that can be instantiated but never used.  It is intended to
 
    be replaced when information is available.
 
    
 
    Usage:
 
    >>> d = DummyClass(1, 2, x="xyzzy")
 
    >>> d.someattr
 
    Traceback (most recent call last):
 
      File "<stdin>", line 1, in ?
 
      File "Utility.py", line 33, in __getattr__
 
        raise AttributeError, "Attempted usage of a DummyClass: %s" % key
 
    AttributeError: Attempted usage of a DummyClass: someattr
 
    >>> d.somefunction()
 
    Traceback (most recent call last):
 
      File "<stdin>", line 1, in ?
 
      File "Utility.py", line 33, in __getattr__
 
        raise AttributeError, "Attempted usage of a DummyClass: %s" % key
 
    AttributeError: Attempted usage of a DummyClass: somefunction"""
 

	
 
    def __init__(self, use_warnings=1, raise_exceptions=0, **kw):
 
        """Constructs a DummyClass"""
 
        make_attributes_from_args('use_warnings', 'raise_exceptions')
 

	
 
    def __getattr__(self, key):
 
        """Raises an exception to warn the user that a Dummy is not being
 
        replaced in time."""
 
        if key == "__del__":
 
            return
 
        msg = "Attempted usage of '%s' on a DummyClass" % key
 
        if self.use_warnings:
 
            import warnings
 
            warnings.warn(msg)
 
        if self.raise_exceptions:
 
            raise AttributeError(msg)
 
        return lambda *args, **kw: self.bogus_function()
 

	
 
    def bogus_function(self):
 
        pass
 

	
 

	
 
class ClassyDict(dict):
 
    """A dict that accepts attribute-style access as well (for keys
 
    that are legal names, obviously). I used to call this Struct, but
 
    chose the more colorful name to avoid confusion with the struct
 
    module."""
 

	
 
    def __getattr__(self, a):
 
        return self[a]
 

	
 
    def __setattr__(self, a, v):
 
        self[a] = v
 

	
 
    def __delattr__(self, a):
 
        del self[a]
 

	
 

	
 
def trace(func):
 
    """Good old fashioned Lisp-style tracing.  Example usage:
 
    
 
    >>> def f(a, b, c=3):
 
    >>>     print a, b, c
 
    >>>     return a + b
 
    >>>
 
    >>>
 
    >>> f = trace(f)
 
    >>> f(1, 2)
 
    |>> f called args: [1, 2]
 
    1 2 3
 
    <<| f returned 3
 
    3
 

	
 
    TODO: print out default keywords (maybe)
 
          indent for recursive call like the lisp version (possible use of 
 
              generators?)"""
 
    name = func.__name__
 

	
 
    def tracer(*args, **kw):
 
        s = '|>> %s called' % name
 
        if args:
 
            s += ' args: %r' % list(args)
 
        if kw:
 
            s += ' kw: %r' % kw
 
        print(s)
 
        ret = func(*args, **kw)
 
        print('<<| %s returned %s' % (name, ret))
 
        return ret
 

	
 
    return tracer
 

	
 

	
 
# these functions taken from old light8 code
 
def dict_max(*dicts):
 
    """
 
    ({'a' : 5, 'b' : 9}, {'a' : 10, 'b' : 4})
 
      returns ==> {'a' : 10, 'b' : 9}
 
    """
 
    newdict = {}
 
    for d in dicts:
 
        for k,v in list(d.items()):
 
            newdict[k] = max(v, newdict.get(k, 0))
 
    return newdict
 

	
 

	
 
def dict_scale(d,scl):
 
    """scales all values in dict and returns a new dict"""
 
    return dict([(k,v*scl) for k,v in list(d.items())])
 
    
 

	
 
def dict_subset(d, dkeys, default=0):
 
    """Subset of dictionary d: only the keys in dkeys.  If you plan on omitting
 
    keys, make sure you like the default."""
 
    newd = {} # dirty variables!
 
    for k in dkeys:
 
        newd[k] = d.get(k, default)
 
    return newd
 

	
 

	
 
# functions specific to Timeline
 
# TBD
 
def last_less_than(array, x):
 
    """array must be sorted"""
 
    best = None
 
    for elt in array:
 
        if elt <= x:
 
            best = elt
 
        elif best is not None:
 
            return best
 
    return best
 

	
 

	
 
# TBD
 
def first_greater_than(array, x):
 
    """array must be sorted"""
 
    array_rev = array[:]
 
    array_rev.reverse()
 
    best = None
 
    for elt in array_rev:
 
        if elt >= x:
 
            best = elt
 
        elif best is not None:
 
            return best
 
    return best
 

	
 

	
light9/chase.py
Show inline comments
 

	
 

	
 

	
 
def chase(t,
 
          ontime=0.5,
 
          offset=0.2,
 
          onval=1.0,
 
          offval=0.0,
 
          names=None,
 
          combiner=max):
 
    names = names or []
 
    # maybe this is better:
 
    # period = ontime + ((offset + ontime) * (len(names) - 1))
 
    period = (offset + ontime) * len(names)
 
    outputs = {}
 
    for index, name in enumerate(names):
 
        # normalize our time
 
        local_offset = (offset + ontime) * index
 
        local_t = t - local_offset
 
        local_t %= period
 

	
 
        # see if we're still in the on part
 
        if local_t <= ontime:
 
            value = onval
 
        else:
 
            value = offval
 

	
light9/showconfig.py
Show inline comments
 
import logging, warnings
 
from twisted.python.filepath import FilePath
 
from os import path, getenv
 
from rdflib import Graph
 
from rdflib import URIRef
 
from .namespaces import MUS, L9
 
log = logging.getLogger('showconfig')
 

	
 
_config = None # graph
 

	
 

	
 
def getGraph():
 
    warnings.warn("code that's using showconfig.getGraph should be "
 
                  "converted to use the sync graph", stacklevel=2)
 
    warnings.warn(
 
        "code that's using showconfig.getGraph should be "
 
        "converted to use the sync graph",
 
        stacklevel=2)
 
    global _config
 
    if _config is None:
 
        graph = Graph()
 
        # note that logging is probably not configured the first time
 
        # we're in here
 
        warnings.warn("reading n3 files around %r" % root())
 
        for f in FilePath(root()).globChildren("*.n3") + FilePath(root()).globChildren("build/*.n3"):
 
        for f in FilePath(root()).globChildren("*.n3") + FilePath(
 
                root()).globChildren("build/*.n3"):
 
            graph.parse(location=f.path, format='n3')
 
        _config = graph
 
    return _config
 

	
 

	
 
def root():
 
    r = getenv("LIGHT9_SHOW")
 
    if r is None:
 
        raise OSError(
 
            "LIGHT9_SHOW env variable has not been set to the show root")
 
    return r
 

	
 

	
 
_showUri = None
 

	
 

	
 
def showUri():
 
    """Return the show URI associated with $LIGHT9_SHOW."""
 
    global _showUri
 
    if _showUri is None:
 
        _showUri = URIRef(open(path.join(root(), 'URI')).read().strip())
 
    return _showUri
 

	
 

	
 
def songOnDisk(song):
 
    """given a song URI, where's the on-disk file that mpd would read?"""
 
    graph = getGraph()
 
    root = graph.value(showUri(), L9['musicRoot'])
 
    if not root:
 
        raise ValueError("%s has no :musicRoot" % showUri())
 

	
 
    name = graph.value(song, L9['songFilename'])
 
    if not name:
 
        raise ValueError("Song %r has no :songFilename" % song)
 

	
 
    return path.abspath(path.join(root, name))
 

	
 

	
 
def songFilenameFromURI(uri):
 
    """
 
    'http://light9.bigasterisk.com/show/dance2007/song8' -> 'song8'
 

	
 
    everything that uses this should be deprecated for real URIs
 
    everywhere"""
 
    assert isinstance(uri, URIRef)
 
    return uri.split('/')[-1]
 

	
 

	
 
def getSongsFromShow(graph, show):
 
    playList = graph.value(show, L9['playList'])
 
    if not playList:
 
        raise ValueError("%r has no l9:playList" % show)
 
    # The patch in https://github.com/RDFLib/rdflib/issues/305 fixed a
 
    # serious bug here.
 
    songs = list(graph.items(playList))
 

	
 
    return songs
 

	
 

	
 
def curvesDir():
 
    return path.join(root(),"curves")
 

	
 

	
 
def subFile(subname):
 
    return path.join(root(),"subs",subname)
 

	
 

	
 
def subsDir():
 
    return path.join(root(),'subs')
light9/subclient.py
Show inline comments
 
from light9.collector.collector_client import sendToCollector
 
from twisted.internet import reactor, task
 
import traceback
 
import time
 
import logging
 
log = logging.getLogger()
 

	
 

	
 
class SubClient:
 

	
 
    def __init__(self):
 
        """assumed that your init saves self.graph"""
 
        pass # we may later need init code for network setup
 

	
 
    def get_levels_as_sub(self):
 
        """Subclasses must implement this method and return a Submaster
 
        object."""
 

	
 
    def send_levels(self):
 
        self._send_sub()
 

	
 
    def send_levels_loop(self, delay=1000):
 
        now = time.time()
 

	
 
        def done(sec):
 
            reactor.callLater(max(0, time.time() - (now + delay)),
 
            reactor.callLater(max(0,
 
                                  time.time() - (now + delay)),
 
                              self.send_levels_loop)
 

	
 
        def err(e):
 
            log.warn('subclient loop: %r', e)
 
            reactor.callLater(2, self.send_levels_loop)
 
            
 
        d = self._send_sub()
 
        d.addCallbacks(done, err)
 

	
 

	
 
    def _send_sub(self):
 
        try:
 
            with self.graph.currentState() as g:
 
                outputSettings = self.get_output_settings(_graph=g)
 
        except:
 
            traceback.print_exc()
 
            return
 
        return sendToCollector('subclient', self.session, outputSettings)
light9/tkdnd.py
Show inline comments
 
from glob import glob
 
from os.path import join, basename
 

	
 

	
 
class TkdndEvent(object):
 
    """
 
    see http://www.ellogon.org/petasis/tcltk-projects/tkdnd/tkdnd-man-page
 
    for details on the fields
 

	
 
    The longer attribute names (action instead of %A) were made up for
 
    this API.
 

	
 
    Not all attributes are visible yet, since I have not thought
 
    through what conversions they should receive and I don't want to
 
    unnecessarily change their types later.
 
    """
 
    substitutions = {
 
        "%A" : "action",
 
        "%b" : "button",
 
        "%D" : "data",
 
        "%m" : "modifiers",
 
        "%T" : "type",
 
        "%W" : "targetWindow",
 
        "%X" : "mouseX",
 
        "%Y" : "mouseY",
 
        }
 

	
 
    @classmethod
 
    def makeEvent(cls, *args):
 
        ev = cls()
 
        for (k, v), arg in zip(sorted(TkdndEvent.substitutions.items()), args):
 
            setattr(ev, v, arg)
 
        # it would be cool for this to decode text data according to the charset in the type
 
        for attr in ['button', 'mouseX', 'mouseY']:
 
            setattr(ev, attr, int(getattr(ev, attr)))
 
        return (ev,)
 

	
 
    tclSubstitutions = ' '.join(sorted(substitutions.keys()))
 

	
 
    def __repr__(self):
 
        return "<TkdndEvent %r>" % self.__dict__
 

	
 

	
 
class Hover(object):
 

	
 
    def __init__(self, widget, style):
 
        self.widget, self.style = widget, style
 
        self.oldStyle = {}
 

	
 
    def set(self, ev):
 
        for k, v in list(self.style.items()):
 
            self.oldStyle[k] = self.widget.cget(k)
 
        self.widget.configure(**self.style)
 
        return ev.action
 

	
 
    def restore(self, ev):
 
        self.widget.configure(**self.oldStyle)
 

	
 

	
 
def initTkdnd(tk, tkdndBuildDir):
 
    """
 
    pass the 'tk' attribute of any Tkinter object, and the top dir of
 
    your built tkdnd package
 
    """
 
    tk.call('source', join(tkdndBuildDir, 'library/tkdnd.tcl'))
 
    for dll in glob(join(tkdndBuildDir,
 
    for dll in glob(
 
            join(tkdndBuildDir,
 
                         '*tkdnd*' + tk.call('info', 'sharedlibextension'))):
 
        tk.call('tkdnd::initialise',
 
                join(tkdndBuildDir, 'library'),
 
                join('..', basename(dll)),
 
                'tkdnd')
 
        tk.call('tkdnd::initialise', join(tkdndBuildDir, 'library'),
 
                join('..', basename(dll)), 'tkdnd')
 

	
 
def dragSourceRegister(widget,
 
                       action='copy', datatype='text/uri-list', data=''):
 

	
 
def dragSourceRegister(widget, action='copy', datatype='text/uri-list',
 
                       data=''):
 
    """
 
    if the 'data' param is callable, it will be called every time to
 
    look up the current data.
 

	
 
    If the callable returns None (or data is None to begin with), the drag
 
    """
 
    widget.tk.call('tkdnd::drag_source', 'register', widget._w)
 

	
 
    # with normal Tkinter bind(), the result of your handler isn't
 
    # actually returned so the drag doesn't get launched. This is a
 
    # corrected version of what bind() does when you pass a function,
 
    # but I don't block my tuple from getting returned (as a tcl list)
 

	
 
    def init():
 
        dataValue = data() if callable(data) else data
 
        if dataValue is None:
 
            return
 
        return (action, datatype, dataValue)
 

	
 
    funcId = widget._register(init,
 
    funcId = widget._register(
 
        init,
 
                              widget._substitute,
 
                              1 # needscleanup
 
                              )
 
    widget.bind("<<DragInitCmd>>", funcId)
 

	
 
def dropTargetRegister(widget, typeList=None,
 

	
 
def dropTargetRegister(
 
        widget,
 
        typeList=None,
 
                       onDropEnter=None,
 
                       onDropPosition=None,
 
                       onDropLeave=None,
 
                       onDrop=None,
 
                       hoverStyle=None,
 
                       ):
 
    """
 
    the optional callbacks will be called with a TkdndEvent
 
    argument.
 

	
 
    onDropEnter, onDropPosition, and onDrop are supposed to return an
 
    action (perhaps the value in event.action). The return value seems
 
    to have no effect, but it might just be that errors are getting
 
    silenced.
 

	
 
    Passing hoverStyle sets onDropEnter to call
 
    widget.configure(**hoverStyle) and onDropLeave to restore the
 
    widget's style. onDrop is also wrapped to do a restore.
 
    """
 

	
 
    if hoverStyle is not None:
 
        hover = Hover(widget, hoverStyle)
 

	
 
        def wrappedDrop(ev):
 
            hover.restore(ev)
 
            if onDrop:
 
                return onDrop(ev)
 
        return dropTargetRegister(widget, typeList=typeList,
 

	
 
        return dropTargetRegister(widget,
 
                                  typeList=typeList,
 
                                  onDropEnter=hover.set,
 
                                  onDropLeave=hover.restore,
 
                                  onDropPosition=onDropPosition,
 
                                  onDrop=wrappedDrop)
 

	
 
    if typeList is None:
 
        typeList = ['*']
 
    widget.tk.call(*(['tkdnd::drop_target', 'register', widget._w]+typeList))
 

	
 
    for sequence, handler in [
 
        ('<<DropEnter>>', onDropEnter),
 
        ('<<DropPosition>>', onDropPosition),
 
        ('<<DropLeave>>', onDropLeave),
 
        ('<<Drop>>', onDrop),
 
        ]:
 
        if not handler:
 
            continue
 
        func = widget._register(handler, subst=TkdndEvent.makeEvent, needcleanup=1)
 
        func = widget._register(handler,
 
                                subst=TkdndEvent.makeEvent,
 
                                needcleanup=1)
 
        widget.bind(sequence, func + " " + TkdndEvent.tclSubstitutions)
 

	
 

	
light9/uihelpers.py
Show inline comments
 
"""all the tiny tk helper functions"""
 

	
 

	
 
#from Tkinter import Button
 
import logging, time
 
from rdflib import Literal
 
from tkinter.tix import Button, Toplevel, Tk, IntVar, Entry, DoubleVar
 
import tkinter
 
from light9.namespaces import L9
 

	
 
log = logging.getLogger("toplevel")
 

	
 
windowlocations = {
 
    'sub' : '425x738+00+00',
 
    'console' : '168x24+848+000',
 
    'leveldisplay' : '144x340+870+400',
 
    'cuefader' : '314x212+546+741',
 
    'effect' : '24x24+0963+338',
 
    'stage' : '823x683+37+030',
 
    'scenes' : '504x198+462+12',
 
}
 

	
 

	
 
def bindkeys(root,key, func):
 
    root.bind(key, func)
 
    for w in root.winfo_children():
 
        w.bind(key, func)
 

	
 

	
 
def toplevel_savegeometry(tl,name):
 
    try:
 
        geo = tl.geometry()
 
        if not geo.startswith("1x1"):
 
            f=open(".light9-window-geometry-%s" % name.replace(' ','_'),'w')
 
            f.write(tl.geometry())
 
        # else the window never got mapped
 
    except Exception as e:
 
        # it's ok if there's no saved geometry
 
        pass
 

	
 

	
 
def toplevelat(name, existingtoplevel=None, graph=None, session=None):
 
    tl = existingtoplevel or Toplevel()
 
    tl.title(name)
 

	
 
    lastSaved = [None]
 
    setOnce = [False]
 
    graphSetTime = [0]
 

	
 
    def setPosFromGraphOnce():
 
        """
 
        the graph is probably initially empty, but as soon as it gives
 
        us one window position, we stop reading them
 
        """
 
        if setOnce[0]:
 
            return
 
        geo = graph.value(session, L9.windowGeometry)
 
        log.debug("setPosFromGraphOnce %s", geo)
 

	
 
        setOnce[0] = True
 
        graphSetTime[0] = time.time()
 
        if geo is not None and geo != lastSaved[0]:
 
            tl.geometry(geo)
 
            lastSaved[0] = geo
 

	
 
    def savePos(ev):
 
        geo = tl.geometry()
 
        if not isinstance(ev.widget, (Tk, tkinter.Tk)):
 
            # I think these are due to internal widget size changes,
 
            # not the toplevel changing
 
            return
 
        # this is trying to not save all the startup automatic window
 
        # sizes. I don't have a better plan for this yet.
 
        if graphSetTime[0] == 0 or time.time() < graphSetTime[0] + 3:
 
            return
 
        if not setOnce[0]:
 
            return
 
        lastSaved[0] = geo
 
        log.debug("saving position %s", geo)
 
        graph.patchObject(session, session, L9.windowGeometry, Literal(geo))
 

	
 
    if graph is not None and session is not None:
 
        graph.addHandler(setPosFromGraphOnce)
 

	
 
    if name in windowlocations:
 
        tl.geometry(positionOnCurrentDesktop(windowlocations[name]))
 

	
 
    if graph is not None:
 
        tl._toplevelat_funcid = tl.bind("<Configure>",
 
                                        lambda ev,tl=tl,name=name: savePos(ev))
 
        tl._toplevelat_funcid = tl.bind(
 
            "<Configure>", lambda ev, tl=tl, name=name: savePos(ev))
 

	
 
    return tl
 

	
 

	
 
def positionOnCurrentDesktop(xform, screenWidth=1920, screenHeight=1440):
 
    size, x, y = xform.split('+')
 
    x = int(x) % screenWidth
 
    y = int(y) % screenHeight
 
    return "%s+%s+%s" % (size, x, y)
 

	
 

	
 
def toggle_slider(s):
 
    if s.get() == 0:
 
        s.set(100)
 
    else:
 
        s.set(0)
 

	
 

	
 
# for lambda callbacks
 
def printout(t):
 
    print('printout', t)
 

	
 

	
 
def printevent(ev):
 
    for k in dir(ev):
 
        if not k.startswith('__'):
 
            print('ev', k, getattr(ev,k))
 

	
 

	
 
def eventtoparent(ev,sequence):
 
    "passes an event to the parent, screws up TixComboBoxes"
 

	
 
    wid_class = str(ev.widget.__class__)
 
    if wid_class == 'Tix.ComboBox' or wid_class == 'Tix.TixSubWidget':
 
        return
 

	
 
    evdict={}
 
    for x in ['state', 'time', 'y', 'x', 'serial']:
 
        evdict[x]=getattr(ev,x)
 

	
 

	
 
#    evdict['button']=ev.num
 
    par=ev.widget.winfo_parent()
 
    if par!=".":
 
        ev.widget.nametowidget(par).event_generate(sequence,**evdict)
 
    #else the event made it all the way to the top, unhandled
 

	
 

	
 
def colorlabel(label):
 
    """color a label based on its own text"""
 
    txt=label['text'] or "0"
 
    lev=float(txt)/100
 
    low=(80,80,180)
 
    high=(255,55,0o50)
 
    out = [int(l+lev*(h-l)) for h,l in zip(high,low)]
 
    col="#%02X%02X%02X" % tuple(out)
 
    label.config(bg=col)
 

	
 

	
 
# TODO: get everyone to use this
 
def colorfade(low, high, percent):
 
    '''not foolproof.  make sure 0 < percent < 1'''
 
    out = [int(l+percent*(h-l)) for h,l in zip(high,low)]
 
    col="#%02X%02X%02X" % tuple(out)
 
    return col
 

	
 

	
 
def colortotuple(anytkobj, colorname):
 
    'pass any tk object and a color name, like "yellow"'
 
    rgb = anytkobj.winfo_rgb(colorname)
 
    return [v / 256 for v in rgb]
 

	
 

	
 
class Togglebutton(Button):
 
    """works like a single radiobutton, but it's a button so the
 
    label's on the button face, not to the side. the optional command
 
    callback is called on button set, not on unset. takes a variable
 
    just like a checkbutton"""
 
    def __init__(self,parent,variable=None,command=None,downcolor='red',**kw):
 

	
 
    def __init__(self,
 
                 parent,
 
                 variable=None,
 
                 command=None,
 
                 downcolor='red',
 
                 **kw):
 

	
 
        self.oldcommand = command
 
        Button.__init__(self,parent,command=self.invoke,**kw)
 

	
 
        self._origbkg = self.cget('bg')
 
        self.downcolor = downcolor
 

	
 
        self._variable = variable
 
        if self._variable:
 
            self._variable.trace('w',self._varchanged)
 
            self._setstate(self._variable.get())
 
        else:
 
            self._setstate(0)
 

	
 
        self.bind("<Return>",self.invoke)
 
        self.bind("<1>",self.invoke)
 
        self.bind("<space>",self.invoke)
 

	
 
    def _varchanged(self,*args):
 
        self._setstate(self._variable.get())
 

	
 
    def invoke(self,*ev):
 
        if self._variable:
 
            self._variable.set(not self.state)
 
        else:
 
            self._setstate(not self.state)
 

	
 
        if self.oldcommand and self.state: # call command only when state goes to 1
 
            self.oldcommand()
 
        return "break"
 

	
 
    def _setstate(self,newstate):
 
        self.state = newstate
 
        if newstate: # set
 
            self.config(bg=self.downcolor,relief='sunken')
 
        else: # unset
 
            self.config(bg=self._origbkg,relief='raised')
 
        return "break"
 

	
 

	
 
class FancyDoubleVar(DoubleVar):
 

	
 
    def __init__(self,master=None):
 
        DoubleVar.__init__(self,master)
 
        self.callbacklist = {} # cbname : mode
 
        self.namedtraces = {} # name : cbname
 

	
 
    def trace_variable(self,mode,callback):
 
        """Define a trace callback for the variable.
 

	
 
        MODE is one of "r", "w", "u" for read, write, undefine.
 
        CALLBACK must be a function which is called when
 
        the variable is read, written or undefined.
 

	
 
        Return the name of the callback.
 
        """
 
        cbname = self._master._register(callback)
 
        self._tk.call("trace", "variable", self._name, mode, cbname)
 

	
 
        # we build a list of the trace callbacks (the py functrions and the tcl functionnames)
 
        self.callbacklist[cbname] = mode
 
#        print "added trace:",callback,cbname
 

	
 
        return cbname
 

	
 
    trace=trace_variable
 

	
 
    def disable_traces(self):
 
        for cb,mode in list(self.callbacklist.items()):
 
#            DoubleVar.trace_vdelete(self,v[0],k)
 
            self._tk.call("trace", "vdelete", self._name, mode,cb)
 
            # but no master delete!
 

	
 
    def recreate_traces(self):
 
        for cb,mode in list(self.callbacklist.items()):
 
#            self.trace_variable(v[0],v[1])
 
            self._tk.call("trace", "variable", self._name, mode,cb)
 

	
 
    def trace_named(self, name, callback):
 
        if name in self.namedtraces:
 
            print("FancyDoubleVar: already had a trace named %s - replacing it" % name)
 
            print(
 
                "FancyDoubleVar: already had a trace named %s - replacing it" %
 
                name)
 
            self.delete_named(name)
 

	
 
        cbname = self.trace_variable('w',callback) # this will register in self.callbacklist too
 
        cbname = self.trace_variable(
 
            'w', callback)  # this will register in self.callbacklist too
 

	
 
        self.namedtraces[name] = cbname
 
        return cbname
 

	
 
    def delete_named(self, name):
 
        if name in self.namedtraces:
 

	
 
            cbname = self.namedtraces[name]
 

	
 
            self.trace_vdelete('w',cbname)
 
	    #self._tk.call("trace","vdelete",self._name,'w',cbname)
 
            print("FancyDoubleVar: successfully deleted trace named %s" % name)
 
        else:
 
            print("FancyDoubleVar: attempted to delete named %s which wasn't set to any function" % name)
 
            print(
 
                "FancyDoubleVar: attempted to delete named %s which wasn't set to any function"
 
                % name)
 

	
 

	
 
def get_selection(listbox):
 
    'Given a listbox, returns first selection as integer'
 
    selection = int(listbox.curselection()[0]) # blech
 
    return selection
 

	
 

	
 
if __name__=='__main__':
 
    root=Tk()
 
    root.tk_focusFollowsMouse()
 
    iv=IntVar()
 

	
 
    def cb():
 
        print("cb!")
 

	
 
    t = Togglebutton(root,text="testbutton",command=cb,variable=iv)
 
    t.pack()
 
    Entry(root,textvariable=iv).pack()
 
    root.mainloop()
light9/updatefreq.py
Show inline comments
 
"""calculates your updates-per-second"""
 

	
 
import time
 

	
 
import time
 

	
 
class Updatefreq:
 
    """make one of these, call update() on it as much as you want,
 
    and then float() or str() the object to learn the updates per second.
 

	
 
    the samples param to __init__ specifies how many past updates will
 
    be stored.  """
 
    
 
    def __init__(self,samples=20):
 
        self.times=[0]
 
        self.samples=samples
 

	
 
    def update(self):
 

	
 
        """call this every time you do an update"""
 
        self.times=self.times[-self.samples:]
 
        self.times.append(time.time())
 

	
 
    def __float__(self):
 
        
 
        """a cheap algorithm, for now, which looks at the first and
 
        last times only"""
 

	
 
        try:
 
            hz=len(self.times)/(self.times[-1]-self.times[0])
 
        except ZeroDivisionError:
 
            return 0.0
 
        return hz
 

	
 
    def __str__(self):
 
        return "%.2fHz"%float(self)
light9/wavelength.py
Show inline comments
 
#!/usr/bin/python
 

	
 
import sys, wave
 

	
 
import sys, wave
 

	
 
def wavelength(filename):
 
    filename = filename.replace('.ogg', '.wav')
 
    wavefile = wave.open(filename, 'rb')
 

	
 
    framerate = wavefile.getframerate() # frames / second
 
    nframes = wavefile.getnframes() # number of frames
 
    song_length = nframes / framerate
 

	
 
    return song_length
 

	
 

	
 
if __name__ == "__main__":
 
    for songfile in sys.argv[1:]:
 
        print(songfile, wavelength(songfile))
light9/wavepoints.py
Show inline comments
 
import wave, audioop
 

	
 
import wave, audioop
 

	
 
def simp(filename, seconds_per_average=0.001):
 
    """smaller seconds_per_average means fewer data points"""
 
    wavefile = wave.open(filename, 'rb')
 
    print("# gnuplot data for %s, seconds_per_average=%s" % \
 
        (filename, seconds_per_average))
 
    print("# %d channels, samplewidth: %d, framerate: %s, frames: %d\n# Compression type: %s (%s)" % wavefile.getparams())
 
    print(
 
        "# %d channels, samplewidth: %d, framerate: %s, frames: %d\n# Compression type: %s (%s)"
 
        % wavefile.getparams())
 

	
 
    framerate = wavefile.getframerate() # frames / second
 
    frames_to_read = int(framerate * seconds_per_average)
 
    print("# frames_to_read=%s" % frames_to_read)
 

	
 
    time_and_max = []
 
    values = []
 
    count = 0
 
    while True:
 
        fragment = wavefile.readframes(frames_to_read)
 
        if not fragment:
 
            break
 

	
 
        # other possibilities:
 
        # m = audioop.avg(fragment, 2)
 
        # print count, "%s %s" % audioop.minmax(fragment, 2)
 

	
 
        m = audioop.rms(fragment, wavefile._framesize)
 
        time_and_max.append((count, m))
 
        values.append(m)
 
        count += frames_to_read
 
        # if count>1000000:
 
            # break
 

	
 
    # find the min and max
 
    min_value, max_value = min(values), max(values)
 
    points = [] # (secs,height)
 
    for count, value in time_and_max:
 
        points.append((count/framerate,
 
                       (value - min_value) / (max_value - min_value)))
 
        points.append(
 
            (count / framerate, (value - min_value) / (max_value - min_value)))
 
    return points
0 comments (0 inline, 0 general)