summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--blc2/__init__.py (renamed from __init__.py)0
-rw-r--r--blc2/constants.py69
-rw-r--r--blc2/exceptions.py (renamed from exceptions.py)0
-rw-r--r--blc2/functions/__init__.py (renamed from functions/__init__.py)0
-rw-r--r--blc2/functions/audio.py (renamed from functions/audio.py)76
-rw-r--r--blc2/functions/function.py (renamed from functions/function.py)44
-rw-r--r--blc2/functions/scene.py (renamed from functions/scene.py)21
-rw-r--r--blc2/interfaces.py (renamed from interfaces.py)0
-rw-r--r--blc2/topology.py (renamed from topology.py)2
-rw-r--r--blc2/utility.py6
-rw-r--r--blc2/workspace.py (renamed from workspace.py)34
-rw-r--r--constants.py13
12 files changed, 225 insertions, 40 deletions
diff --git a/__init__.py b/blc2/__init__.py
index e69de29..e69de29 100644
--- a/__init__.py
+++ b/blc2/__init__.py
diff --git a/blc2/constants.py b/blc2/constants.py
new file mode 100644
index 0000000..792a877
--- /dev/null
+++ b/blc2/constants.py
@@ -0,0 +1,69 @@
+"""Constants module.
+
+Contains some constants used throughout.
+"""
+
+class _Infinity:
+ """Class for the singleton INFTY.
+
+ We assume that this is only used in time-related operations, i.e. that numbers involved
+ are nonnegative.
+ """
+ INFTY = None
+
+ def __repr__(self):
+ return "infty"
+
+ def __str__(self):
+ return "infty"
+
+ def __gt__(self, other):
+ return True
+
+ def __lt__(self, other):
+ return False
+
+ def __add__(self, other):
+ return self
+
+ def __radd__(self, other):
+ return self
+
+ def __sub__(self, other):
+ return self
+
+ def __rsub__(self, other):
+ return self
+
+ def __mul__(self, other):
+ if other == 0:
+ return 0
+ return self
+
+ def __rmul__(self, other):
+ if other == 0:
+ return 0
+ return self
+
+ def __init__(self):
+ if _Infinity.INFTY is not None:
+ raise ValueError("Cannot create two infinities")
+ _Infinity.INFTY = self
+
+INFTY = _Infinity()
+
+AUTO = -2
+
+BXW = "{http://unsuspicious.services/bxw}"
+
+SCENE = "Scene"
+AUDIO = "Audio"
+CHASER = "Chaser"
+CHASERSTEP = "ChaserStep"
+FUNCTION = "Function"
+
+INTERNAL = "Internal"
+EXTERNAL = "External"
+
+INHERIT = "Inherit"
+MANUAL = "Manual"
diff --git a/exceptions.py b/blc2/exceptions.py
index 1b950e8..1b950e8 100644
--- a/exceptions.py
+++ b/blc2/exceptions.py
diff --git a/functions/__init__.py b/blc2/functions/__init__.py
index e69de29..e69de29 100644
--- a/functions/__init__.py
+++ b/blc2/functions/__init__.py
diff --git a/functions/audio.py b/blc2/functions/audio.py
index d0e599d..e41806e 100644
--- a/functions/audio.py
+++ b/blc2/functions/audio.py
@@ -7,7 +7,7 @@ import xml.etree.ElementTree as et
from .function import Function
-from ..constants import AUDIO, BXW
+from ..constants import AUDIO, BXW, INTERNAL
from ..exceptions import LoadError
class Audio(Function):
@@ -19,25 +19,79 @@ class Audio(Function):
The duration of the audio is automatically determined and is zero if the file does not
exist or is unsupported by ffmpeg.
"""
- scope = ()
- audio_scope = frozenset()
- actual_duration = 0
+ type = AUDIO
+ fade_out_mode = INTERNAL
def __init__(self, w, id_ = None, name = None, fade_in = 0, fade_out = 0,
filename: str = None):
- super().__init__(w=w, type_=AUDIO, id_=id_, name=name, duration=0,
- fade_in=fade_in, fade_out=fade_out)
+ super().__init__(w=w, id_=id_, name=name)
self._filename = filename
+ if fade_in < 0 or fade_out < 0:
+ raise ValueError("Fades must be nonnegative")
+ self._fade_out = fade_out
+ self._fade_in = fade_in
+
if filename is not None:
- self.duration = self.w.get_audio_length(filename)
- self.audio_scope = frozenset(((filename,),))
+ self._duration = self.w.get_audio_length(filename)
+ self._audio_scope = frozenset(((filename,),))
else:
- self.duration = 0
- self.audio_scope = frozenset()
+ self._duration = 0
+ self._audio_scope = frozenset()
+
+ @property
+ def fade_in(self):
+ return self._fade_in
+
+ @fade_in.setter
+ def fade_in(self, v):
+ if v < 0:
+ raise ValueError("Fades must be nonnegative")
+
+ if v != self._fade_in:
+ self._fade_in = v
+ self.w.function_changed(self)
+
+ @property
+ def fade_out(self):
+ return self._fade_out
- self.actual_duration = self.duration
+ @fade_out.setter
+ def fade_out(self, v):
+ if v < 0:
+ raise ValueError("Fades must be nonnegative")
+
+ if v != self._fade_out:
+ self._fade_out = v
+ self.w.function_changed(self)
+
+ @property
+ def scope(self):
+ return ()
+
+ @property
+ def audio_scope(self):
+ return self._audio_scope.union()
+
+ @property
+ def actual_duration(self):
+ return self._duration
+
+ @property
+ def duration(self):
+ return self._duration
+
+ def _set_duration(self, value):
+ """Set the duration.
+
+ This is called by Workspace when the duration for the file has been rechecked; do
+ not call this elsewhere, duration is intended to be immutable from outside the
+ library.
+ """
+ if value != self._duration:
+ self._duration = value
+ self.w.function_changed(self)
def get_data(self):
return None
diff --git a/functions/function.py b/blc2/functions/function.py
index 97c98f7..d210936 100644
--- a/functions/function.py
+++ b/blc2/functions/function.py
@@ -7,7 +7,7 @@ from abc import ABCMeta, abstractmethod, abstractproperty
from typing import Set, Any
from ..topology import Fixture
-from ..constants import INFTY
+from ..constants import INFTY, EXTERNAL, FUNCTION
from ..interfaces import XMLSerializable
class Function(XMLSerializable, metaclass=ABCMeta):
@@ -23,22 +23,48 @@ class Function(XMLSerializable, metaclass=ABCMeta):
reported using ``Workspace.function_changed`` with the new values. This should also be
extended to any change in e.g. values for lighting and audio primitives.
"""
- def __init__(self, w: "Workspace", type_: str, id_: int = None, name: str = None,
- duration: int = INFTY, fade_in: int = 0, fade_out: int = 0):
+ type: FUNCTION
+ fade_out_mode: EXTERNAL
+
+ def __init__(self, w: "Workspace", id_: int = None, name: str = None):
self.w = w
- self.id = id_ if id_ is not None else w.next_function_id
- self.name = name if name else "%s %s" % (type_, self.id)
- self.type = type_
- self.duration = duration
- self.fade_in = fade_in
- self.fade_out = fade_out
+ self._id = id_ if id_ is not None else w.next_function_id
+ self._name = name if name else "%s %s" % (self.type, self.id)
self.w.register_function(self)
+ @property
+ def id(self):
+ """Return the function's ID."""
+ return self._id
+
+ @property
+ def name(self):
+ """Return the function's name."""
+ return self._name
+
+ @name.setter
+ def name(self, v):
+ if v != self._name:
+ self._name = v if v else "%s %s" % (self.type, self.id)
+ self.w.function_changed(self)
+
def delete(self):
"""Delete the function from the Workspace."""
self.w.function_deleted(self)
+ @abstractproperty
+ def duration(self) -> int:
+ """Return the function's duration (excluding fades)."""
+
+ @abstractproperty
+ def fade_in(self) -> int:
+ """Return the function's fade in time."""
+
+ @abstractproperty
+ def fade_out(self) -> int:
+ """Return the function's fade out time."""
+
@abstractproperty
def scope(self) -> Set[Fixture.Channel]:
"""Return the set of channels affected by this function."""
diff --git a/functions/scene.py b/blc2/functions/scene.py
index 958138f..ddda1c5 100644
--- a/functions/scene.py
+++ b/blc2/functions/scene.py
@@ -28,17 +28,32 @@ class Scene(Function):
"""
audio_scope = ()
scope = frozenset()
- actual_duration = INFTY
values = frozenset()
+ type = SCENE
def __init__(self, w, id_ = None, name = None, values: Mapping[Fixture.Channel, int] = None):
- super().__init__(w=w, type_=SCENE, id_=id_, name=name, duration=INFTY, fade_in=0,
- fade_out=0)
+ super().__init__(w=w, id_=id_, name=name)
self._values = {}
self._render = ()
self._update(values, changed=False)
+ @property
+ def fade_in(self):
+ return 0
+
+ @property
+ def fade_out(self):
+ return 0
+
+ @property
+ def duration(self):
+ return INFTY
+
+ @property
+ def actual_duration(self):
+ return INFTY
+
def _update_render(self, changed=True):
self._render = tuple(self._values.items())
self.scope = frozenset(self._values.keys())
diff --git a/interfaces.py b/blc2/interfaces.py
index e0a904a..e0a904a 100644
--- a/interfaces.py
+++ b/blc2/interfaces.py
diff --git a/topology.py b/blc2/topology.py
index 331c4e2..e0e0ea4 100644
--- a/topology.py
+++ b/blc2/topology.py
@@ -10,6 +10,8 @@ from .constants import BXW
from .interfaces import XMLSerializable
from .exceptions import LoadError
+## TODO: Call Workspace.topology_changed when changed
+
class Fixture(XMLSerializable):
"""Class representing a lighting fixture.
diff --git a/blc2/utility.py b/blc2/utility.py
new file mode 100644
index 0000000..9fc47a4
--- /dev/null
+++ b/blc2/utility.py
@@ -0,0 +1,6 @@
+"""Module containing various utility functions."""
+
+import datetime as dt
+
+def fromisoformat(s):
+ return dt.datetime.strptime(s, "%Y-%m-%dT%H:%M:%S.%f")
diff --git a/workspace.py b/blc2/workspace.py
index add047b..51b35af 100644
--- a/workspace.py
+++ b/blc2/workspace.py
@@ -12,6 +12,7 @@ from .constants import AUDIO, SCENE, BXW
from .functions.function import Function
from .exceptions import LoadError
from .interfaces import XMLSerializable
+from .utility import fromisoformat
def ffprobe_audio_length(f: str, path: str = "ffprobe") -> int:
"""Use ffprobe to check audio length in milliseconds.
@@ -29,7 +30,12 @@ def ffprobe_audio_length(f: str, path: str = "ffprobe") -> int:
return int(1000*float(json.loads(a)["format"]["duration"])+0.5)
class Workspace(XMLSerializable):
- """Class representing a audiovisual workspace."""
+ """Class representing a audiovisual workspace.
+
+ Note that all callbacks are executed synchronously: if they require long periods of
+ time to execute, the callback should handle scheduling the actual work on a different
+ thread.
+ """
def __init__(self, name: str, author: str, version: int, modified: dt.datetime):
self.name = name
self.author = author
@@ -45,6 +51,7 @@ class Workspace(XMLSerializable):
self._change_callbacks = {}
self._delete_callbacks = {}
+ self._topology_callbacks = []
def get_audio_length(self, filename: str) -> int:
"""Determine the audio length of the given file.
@@ -64,7 +71,7 @@ class Workspace(XMLSerializable):
self._audio_lengths[filename] = ffprobe_audio_length(filename)
for f in self.functions.values():
if f.type == AUDIO and f.filename == filename:
- f.duration = self._audio_lengths[f.filename]
+ f._set_duration(self._audio_lengths[f.filename]) #pylint: disable=protected-access
return self._audio_lengths[filename]
def recheck_audio_lengths(self):
@@ -73,7 +80,7 @@ class Workspace(XMLSerializable):
self._audio_lengths[filename] = ffprobe_audio_length(filename)
for f in self.functions.values():
if f.type == AUDIO and f.filename in self._audio_lengths:
- f.filename = f.filename
+ f._set_duration(self._audio_lengths[f.filename]) #pylint: disable=protected-access
def register_fixture(self, f: "Fixture"):
"""Register the fixture in the Workspace.
@@ -125,6 +132,7 @@ class Workspace(XMLSerializable):
for g in self._delete_callbacks:
if f is None or g == f:
self._delete_callbacks[g] = [i for i in self._delete_callbacks[g] if i[0] != owner]
+ self._topology_callbacks = [i for i in self._topology_callbacks if i[0] != owner]
def register_function_change_callback(self, f: int, callback, owner = None):
"""Register a callback for a function change.
@@ -152,6 +160,24 @@ class Workspace(XMLSerializable):
self._delete_callbacks[f] = []
self._delete_callbacks[f].append((owner, callback))
+ def register_topology_callback(self, callback, owner = None):
+ """Register a callback for a topology change.
+
+ For simplicity, this is fired whenever any change to the topology is made. As
+ functions is general do not care about the topology (only the channels, the
+ deletion of which is handled elsewhere) this function is not concerned about
+ performance.
+
+ :param callback: the function to call, accepting the Workspace
+ :param owner: optional ID to use for removal of the callback later
+ """
+ self._topology_callbacks.append((owner, callback))
+
+ def topology_changed(self):
+ """Call when a topology change has been made."""
+ for f in self._topology_callbacks.values():
+ f(self)
+
def function_changed(self, f: Function):
"""Called when a function is changed.
@@ -198,7 +224,7 @@ class Workspace(XMLSerializable):
name = name.text
author = author.text
version = int(version.text)
- modified = dt.datetime.fromisoformat(modified.text)
+ modified = fromisoformat(modified.text)
w = cls(name=name, author=author, version=version, modified=modified)
diff --git a/constants.py b/constants.py
deleted file mode 100644
index 7008ffc..0000000
--- a/constants.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""Constants module.
-
-Contains some constants used throughout.
-"""
-
-INFTY = -1
-AUTO = -2
-
-BXW = "{http://unsuspicious.services/bxw}"
-
-SCENE = "Scene"
-AUDIO = "Audio"
-CHASER = "Chaser"