From fff5e34c9864532b5e38e70b658eccb0ff35d1d3 Mon Sep 17 00:00:00 2001
From: Ben Connors <benconnors@outlook.com>
Date: Thu, 24 Jan 2019 16:35:21 -0500
Subject: A bunch of changes

- Begin work on simple rendering backend
- Define lighting output interface
- Cache hash() value on functions
- Add unique identifier for each audio cue
---
 workspace.py | 85 +++++++++++++++++++++++++++++++-----------------------------
 1 file changed, 44 insertions(+), 41 deletions(-)

(limited to 'workspace.py')

diff --git a/workspace.py b/workspace.py
index ed40634..c9f9a35 100755
--- a/workspace.py
+++ b/workspace.py
@@ -146,15 +146,13 @@ def ffprobe_audio_length(f, path="ffprobe"):
 ## END Utility functions
 
 ## BEGIN Topology classes
-
 class Fixture:
     """Class representing a single light fixture.
 
     May be composed of multiple channels.
     """
     def __hash__(self):
-        return hash((self.name, self.address_start, self.channel_count, self.mode, self.id,
-                     self.universe))
+        return self._hash
                      
     def __repr__(self):
         return "Fixture(id=%d, name=%s, universe=%d, start=%d, channels=%d)" % (self.id, self.name, self.universe.id, self.address_start, self.channel_count)
@@ -166,13 +164,15 @@ class Fixture:
         self.mode = mode
         self.universe = universe
         self.id = id_
+        self._hash = hash((self.name, self.address_start, self.channel_count, self.mode, 
+                           self.id, self.universe))
         self.channels = [Channel(self, i) for i in range(channels)]
 
 class Channel:
     """Class representing a single output channel."""
     def __hash__(self):
-        return hash((self.fixture, self.offset, self.address))
-
+        return self._hash
+                     
     def __repr__(self):
         return "Channel(address=%d)" % (self.address)
 
@@ -183,12 +183,13 @@ class Channel:
         self.offset = offset
         self.address = self.fixture.address_start + offset
         self.universe = self.fixture.universe
+        self._hash = hash((self.fixture, self.offset, self.address))
 
 class ChannelGroup:
     """Class representing a group of output channels."""
     def __hash__(self):
-        return hash(self.id, self.name, self.channels)
-
+        return self._hash
+                     
     def __repr__(self):
         return "ChannelGroup(id=%d, name=%s, channels=(%s))" % (self.id, self.name,
                                                                 ", ".join((repr(c) for c in self.channels)))
@@ -198,10 +199,12 @@ class ChannelGroup:
         self.name = name 
         self.channels = tuple(channels)
 
+        self._hash = hash((self.id, self.name, self.channels))
+
 class Universe:
     """Class representing an output universe."""
     def __hash__(self):
-        return hash((self.id, self.name))
+        return self._hash
 
     def __repr__(self):
         return "Universe(id=%d, name=%s)" % (self.id, self.name)
@@ -210,6 +213,8 @@ class Universe:
         self.id = id_
         self.name = name
 
+        self._hash = hash((self.id, self.name))
+
 ## END Toplogy classes
 
 ## BEGIN Base classes
@@ -235,8 +240,7 @@ class Function(ABC):
     """
     repr_attr = ("id", "name",)
     def __hash__(self):
-        return hash((self.id, self.type, self.name, self.scope, self.hidden, self.duration,
-                     self.actual_duration))
+        return self._hash
 
     @staticmethod
     def get_data():
@@ -271,7 +275,9 @@ class Function(ABC):
             (values, audio cues, next change, data)
 
         Where values is a tuple of (channel, value) elements, audio_cues is a tuple of 
-        (filename, start time, fade in time, fade out time, fade out start) elements,
+        (filename, aid, start time, fade in time, fade out time, fade out start) elements, aid
+        may be used to uniquely identify instances of audio cues.
+
         next_change is the time index of the next lighting change, and data is the state data 
         (None if unused). values must  contain a value for exactly those channels provided in 
         scope.
@@ -298,11 +304,11 @@ class Function(ABC):
         self.actual_duration = min(QLC_INFTY, actual_duration)
         self.scope = tuple(scope)
 
+        self._hash = hash((self.id, self.type, self.name, self.scope, self.hidden, self.duration,
+                           self.actual_duration))
+
 class FadeFunction(Function):
     """QLC function that can fade in/out."""
-    def __hash__(self):
-        return hash((super().__hash__(), self.fade_in, self.fade_out))
-
     def __init__(self, id_, type_, name, scope, hidden=False, duration=-1, actual_duration=-1, fade_in=0, fade_out=0):
         if fade_in >= QLC_INFTY or fade_out >= QLC_INFTY:
             raise ValueError("Fades cannot be infinite")
@@ -310,6 +316,15 @@ class FadeFunction(Function):
         self.fade_in = min(QLC_INFTY, fade_in)
         self.fade_out = min(QLC_INFTY, fade_out)
 
+        self._hash = hash((self._hash, self.fade_in, self.fade_out))
+
+class Advanceable(ABC):
+    """Function that may be advanced."""
+    @abstractmethod
+    def advance(self, data):
+        """Advance the function."""
+        return
+
 ## END Base classes
 
 ## BEGIN Function classes
@@ -317,26 +332,24 @@ class FadeFunction(Function):
 class Audio(FadeFunction):
     """Class for a QLC+ audio function."""
     repr_attr = ("id", "fname", "fade_in", "fade_out",)
-    def __hash__(self):
-        return hash((super().__hash__(), self.fname, self.run_order))
-
     def render(self, t, data=None):
         """Render the audio function.
 
         We do not seek to do anything related to audio in this library: the responsibility for 
         mixing, fading, playing, probing, etc. the audio file is with the specific application. 
-        As such, this function only returns the relevant data for the audio function.
+        As such, this function only returns the relevant data for the audio function.o
         """
         if t > self.duration:
             return (), (), -1, data
 
-        return (), ((0, self.fname, self.fade_in, self.fade_out, self.duration-self.fade_out),), self.duration+1-t, data
+        return (), ((0, self.id, self.fname, self.fade_in, self.fade_out, self.duration-self.fade_out),), self.duration+1-t, data
 
     def __init__(self, id_, name, fname, fade_in, fade_out, length, run_order=SINGLESHOT, hidden=False):
         super().__init__(id_, AUDIO, name, (), hidden=hidden, duration=length, 
                          actual_duration=length, fade_in=fade_in, fade_out=fade_out)
         self.fname = fname
         self.run_order = run_order
+        self._hash = hash((self._hash, self.fname, self.run_order))
 
 class Scene(Function):
     """Class for a QLC Scene.
@@ -346,9 +359,6 @@ class Scene(Function):
     Scenes are mostly meaningless on their own in this context, they must be attached to a 
     chaser/show to do anything.
     """
-    def __hash__(self):
-        return hash((super().__hash__(), self.values))
-
     def render(self, t, data=None):
         """All arguments are unused."""
         return self.values, (), QLC_INFTY, None
@@ -356,6 +366,7 @@ class Scene(Function):
     def __init__(self, id_, name, values, hidden=False):
         super().__init__(id_, SCENE, name, (c for c,v in values), hidden=hidden, duration=-1, actual_duration=-1)
         self.values = tuple(values)
+        self._hash = hash((self._hash, self.values))
 
 class ChaserStep(FadeFunction):
     """A single step in a chaser."""
@@ -367,9 +378,6 @@ class ChaserStep(FadeFunction):
             self.start_time = start_time
             self.end_time = end_time
 
-    def __hash__(self):
-        return hash((super().__hash__(), self.hold, self.function))
-
     def get_data(self, start_time=0):
         return self.ChaserStepData(fd=self.function.get_data(), start_time=start_time, end_time=self.duration)
 
@@ -405,11 +413,12 @@ class ChaserStep(FadeFunction):
             nx = min(nx, data.end_time-t)
 
         nacues = []
-        for f, s, fin, fout, fstart in acues:
+        for s, aid, f, fin, fout, fstart in acues:
             if fstart + fout > self.fade_out + data.end_time:
                 fstart = data.end_time - self.fade_out
                 fout = self.fade_out
-            nacues.append((f, s+data.start_time, max(self.fade_in, fin), fout, fstart))
+            nacues.append((s+data.start_time, hash((self.id, data.start_time, aid)), 
+                           f, max(self.fade_in, fin), fout, fstart))
 
         return (values, mul), tuple(nacues), nx, data
 
@@ -420,8 +429,9 @@ class ChaserStep(FadeFunction):
         self.id = id_
         self.hold = hold
         self.function = function
+        self._hash = hash((self._hash, self.function, self.hold))
 
-class Chaser(Function):
+class Chaser(Function, Advanceable):
     """Class for representing a QLC+ Chaser or Sequence.
 
     Since they essentially do the same thing (Chaser being more general), they have only one 
@@ -433,9 +443,6 @@ class Chaser(Function):
             self.step_data = step_data 
             self.obey_loop = obey_loop 
 
-    def __hash__(self):
-        return hash((super().__hash__(), self.steps, self.run_order, self.direction))
-
     @staticmethod
     def advance(t, data):
         """End the current chaser step. 
@@ -594,19 +601,18 @@ class Chaser(Function):
         self.steps = tuple(steps)
         self.run_order = run_order
         self.direction = direction
+        self._hash = hash((self._hash, self.steps, self.run_order, self.direction))
 
 class ShowFunction(Function):
     """Class for representing a function in a show."""
     repr_attr = ("id", "name", "start_time", ("function", lambda f: f.id))
-    def __hash__(self):
-        return hash((super().__hash__(), self.function, self.start_time))
-
     def render(self, t, data=None):
         if data is None:
             data = self.function.get_data()
         
         values, acues, nx, data = self.function.render(t-self.start_time, data=data)
-        return values, tuple(((at+self.start_time,*others) for at,*others in acues)), nx, data
+        return values, tuple(((at+self.start_time,hash((self.id, self.start_time, aid)),
+                               *others) for at,aid,*others in acues)), nx, data
 
     def __init__(self, id_, name, function, start_time):
         if function.actual_duration >= QLC_INFTY:
@@ -615,13 +621,11 @@ class ShowFunction(Function):
                          actual_duration=function.actual_duration)
         self.function = function
         self.start_time = start_time
+        self._hash = hash((self._hash, self.start_time, self.function))
 
 class ShowTrack(Function):
     """Class for representing a track in a show."""
     repr_attr = ("id", "name", ("functions", lambda fs: ','.join(("%d@%d" % (f.function.id, f.start_time) for f in fs))))
-    def __hash__(self):
-        return hash((super().__hash__(), self.functions))
-            
     def get_data(self):
         return tuple((f.function.get_data() for f in self.functions))
 
@@ -664,12 +668,10 @@ class ShowTrack(Function):
                 dur = f.start_time + f.duration
             scope.update(f.scope)
         super().__init__(id_, "ShowTrack", name, scope, duration=dur, actual_duration=adur)
+        self._hash = hash((self._hash, self.functions))
 
 class Show(Function):
     """Class representing a QLC+ show."""
-    def __hash__(self):
-        return hash((super().__hash__(), self.tracks))
-
     def render(self, t, data=None):
         if t > self.actual_duration:
             return (), (), -1, data
@@ -768,6 +770,7 @@ class Show(Function):
                 adur = t.actual_duration
         super().__init__(id_, SHOW, name, scope, duration=dur, actual_duration=adur)
         self.tracks = tuple(tracks)
+        self._hash = hash((self._hash, self.tracks))
 
 ## END Function classes
 
-- 
cgit v1.2.3