diff options
-rw-r--r-- | blc2/__init__.py (renamed from __init__.py) | 0 | ||||
-rw-r--r-- | blc2/constants.py | 69 | ||||
-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.py | 6 | ||||
-rw-r--r-- | blc2/workspace.py (renamed from workspace.py) | 34 | ||||
-rw-r--r-- | constants.py | 13 |
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" |