diff options
author | Ben Connors <benconnors@outlook.com> | 2019-09-25 23:05:14 -0400 |
---|---|---|
committer | Ben Connors <benconnors@outlook.com> | 2019-09-25 23:05:14 -0400 |
commit | 2b8a53f98c44e6e78d49b7c246731deef75ed6d3 (patch) | |
tree | d85e383c0d54d662ea93384b177a0ad59a40917e /functions | |
parent | 7f85bd8ed84b23fc4e683ab90fc7babe288f1a27 (diff) |
Change module layout; start chaser work
- Fix up callbacks
- Clean up function implementations
- More properties to prevent editing of attributes
- Start work on chasers
- Implement framework for chaser steps
Diffstat (limited to 'functions')
-rw-r--r-- | functions/__init__.py | 0 | ||||
-rw-r--r-- | functions/audio.py | 118 | ||||
-rw-r--r-- | functions/function.py | 88 | ||||
-rw-r--r-- | functions/scene.py | 144 |
4 files changed, 0 insertions, 350 deletions
diff --git a/functions/__init__.py b/functions/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/functions/__init__.py +++ /dev/null diff --git a/functions/audio.py b/functions/audio.py deleted file mode 100644 index d0e599d..0000000 --- a/functions/audio.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Audio function module. - -Contains the definition of the Audio, the audio primitive. -""" - -import xml.etree.ElementTree as et - -from .function import Function - -from ..constants import AUDIO, BXW -from ..exceptions import LoadError - -class Audio(Function): - """Class representing an audio cue. - - This is the primitive for audio, and all sound cues must be based in some manner off of - Audio. This function merely plays a single audio file once, starting at t=0. - - 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 - - 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) - - self._filename = filename - - if filename is not None: - self.duration = self.w.get_audio_length(filename) - self.audio_scope = frozenset(((filename,),)) - else: - self.duration = 0 - self.audio_scope = frozenset() - - self.actual_duration = self.duration - - def get_data(self): - return None - - def copy_data(self, data): - return None - - def render(self, t, data = None): - return ((), - ((self.id, self._filename, 0, self.fade_in, self.duration, self.fade_out),), - None) - - @property - def filename(self): - """Return the current audio filename.""" - return self._filename - - @filename.setter - def filename(self, value: str): - """Set the current audio filename, updating the duration.""" - if value is not None: - self.duration = self.w.get_audio_length(value) - self.audio_scope = frozenset(((value,),)) - else: - self.duration = 0 - self.audio_scope = frozenset() - - self._filename = value - self.w.function_changed(self) - - def serialize(self) -> et.Element: - e = et.Element(BXW+"function") - e.set("type", self.type) - e.set("id", str(self.id)) - e.set("name", self.name) - e.set("fade-in", str(self.fade_in)) - e.set("fade-out", str(self.fade_out)) - if self.filename is not None: - filename = et.SubElement(e, BXW+"filename") - filename.text = self.filename - - return e - - @classmethod - def deserialize(cls, w, e): - if e.tag != BXW+"function": - raise LoadError("Invalid function tag") - elif e.get("type") != AUDIO: - raise LoadError("Load delegated to wrong class (this is a bug)") - - id_ = cls.int_or_none(e.get("id")) - if id_ is None: - raise LoadError("Function tag has invalid/missing ID") - - name = e.get("name") - - fade_in = e.get("fade-in") - try: - fade_in = int(fade_in) if fade_in else 0 - except ValueError: - raise LoadError("Invalid fade in") - - fade_out = e.get("fade-out") - try: - fade_out = int(fade_out) if fade_out else 0 - except ValueError: - raise LoadError("Invalid fade out") - - if len(e) > 1: - raise LoadError("Audio tag can have at most one filename") - elif len(e) == 1: - filename, = e - filename = filename.text - else: - filename = None - - return cls(w=w, id_=id_, name=name, filename=filename, fade_in=fade_in, - fade_out=fade_out) diff --git a/functions/function.py b/functions/function.py deleted file mode 100644 index 97c98f7..0000000 --- a/functions/function.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Base function module. - -Contains the generic Function interface. -""" - -from abc import ABCMeta, abstractmethod, abstractproperty -from typing import Set, Any - -from ..topology import Fixture -from ..constants import INFTY -from ..interfaces import XMLSerializable - -class Function(XMLSerializable, metaclass=ABCMeta): - """Class representing a generic function. - - Many of the properties here should not be implemented as properties for performance - reasons. - - Functions and properties required for rendering, e.g. ``Function.render`` and - ``Function.actual_duration``, should be written to be performant. - - Any change to the scope, audio scope, or actual duration of the function must be - 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): - 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.w.register_function(self) - - def delete(self): - """Delete the function from the Workspace.""" - self.w.function_deleted(self) - - @abstractproperty - def scope(self) -> Set[Fixture.Channel]: - """Return the set of channels affected by this function.""" - - @abstractproperty - def audio_scope(self) -> Set[str]: - """Return the set of audio filenames that may be used by this function.""" - - @abstractmethod - def get_data(self) -> Any: - """Return the default data for this function.""" - - @abstractmethod - def copy_data(self, data: Any) -> Any: - """Duplicate the given data.""" - - @abstractproperty - def actual_duration(self): - """Return the actual duration of the function, including all fades.""" - - @abstractmethod - def render(self, t: int, data: Any = None): - """Render the function at the given time. - - This function must return a 3-tuple: - - (light_cues, audio_cues, new_data) - - Where ``light_cues`` is a iterable of (channel, value) pairs. It is an error for - ``light_cues`` to contain channels other than exactly this Function's scope. - ``audio_cues`` is an iterable of audio cues of the form: - - (guid, filename, start_time, fade_in, end_time, fade_out) - - The ``guid`` is globally unique: that is, it is unique to that specific audio cue, - even if the same file is played multiple times or in different places. Note that - ``end_time`` may be ``INFTY``, in which case the file should be played to its end. - In the case of infinite-duration chasers, the ``end_time`` may change over - subsequent calls to this function. - - Note that ``data`` is not necessarily mutable: ``render`` should be called like: - - lights, sound, data = f.render(t, data) - - :param t: the time to render at, in milliseconds - :param data: the function data to use - """ diff --git a/functions/scene.py b/functions/scene.py deleted file mode 100644 index 958138f..0000000 --- a/functions/scene.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Scene function module. - -Contains the definition of the Scene, the lighting primitive. -""" - -import xml.etree.ElementTree as et -from typing import Mapping - -from .function import Function -from ..topology import Fixture -from ..constants import INFTY, SCENE, BXW -from ..exceptions import LoadError - -class Scene(Function): - """Class representing a lighting scene. - - This is the primitive for lighting, and all light cues must be based in some manner off - of Scene. Scenes are simple, having no fading and infinite duration. - - Modifying the scene can be done in a few ways: the easiest is using the dictionary- - style accesses, e.g. setting the values using: - - scene[channel] = value - - Values may be removed from the scene using the ``del`` operator and retrieved in the - same fashion. Alternatively, values can be updated in bulk using ``Scene.update`` or - set in bulk with ``Scene.set``. - """ - audio_scope = () - scope = frozenset() - actual_duration = INFTY - values = frozenset() - - 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) - self._values = {} - self._render = () - - self._update(values, changed=False) - - def _update_render(self, changed=True): - self._render = tuple(self._values.items()) - self.scope = frozenset(self._values.keys()) - self.values = self._render - - if changed: - self.w.function_changed(self) - - def set(self, v): - """Set the scene's values to the given ones. - - Equivalent to update, except that all non-given values are deleted from the scene. - """ - self._values = {} - self.update(v) - - def _update(self, v, changed=True): - if isinstance(v, dict): - v = v.items() - vn = [] - for c, val in v: - val = int(val) - if val < 0 or val > 255: - raise ValueError("Values must be integers on [0,256)") - vn.append((c, val)) - for c, val in vn: - self._values[c] = val - self._update_render(changed=changed) - - def update(self, v): - """Update the scene's values.""" - self._update(v=v, changed=True) - - def __getitem__(self, c: Fixture.Channel): - return self._values[c] if c in self._values else None - - def __setitem__(self, c: Fixture.Channel, v: int): - self.update(((c, v),)) - - def __delitem__(self, c: Fixture.Channel): - if c in self._values: - del self._values[c] - self._update_render() - - def get_data(self): - return None - - def copy_data(self, data): - return None - - def render(self, t, data = None): - return (self._render, (), None) - - def serialize(self): - e = et.Element(BXW+"function") - e.set("type", self.type) - e.set("id", str(self.id)) - e.set("name", self.name) - for c, v in self.values: - ce = et.SubElement(e, BXW+"value") - ce.set("fixture", str(c.f.id)) - ce.set("channel", str(c.id)) - ce.text = str(v) - - return e - - @classmethod - def deserialize(cls, w, e): - if e.tag != BXW+"function": - raise LoadError("Invalid function tag") - elif e.get("type") != SCENE: - raise LoadError("Load delegated to wrong class (this is a bug)") - - id_ = cls.int_or_none(e.get("id")) - if id_ is None: - raise LoadError("Function tag has invalid/missing ID") - - name = e.get("name") - - values = {} - for ve in e: - if ve.tag != BXW+"value": - raise LoadError("Invalid value tag") - - fixture = cls.int_or_none(ve.get("fixture")) - channel = cls.int_or_none(ve.get("channel")) - - if None in (fixture, channel): - raise LoadError("Missing/invalid fixture/channel value") - elif fixture not in w.fixtures: - raise LoadError("Missing fixture ID %d" % fixture) - elif channel >= len(w.fixtures[fixture].channels): - raise LoadError("Fixture %d missing channel ID %d" % (fixture, channel)) - - channel = w.fixtures[fixture].channels[channel] - - value = cls.int_or_none(ve.text) - if value is None: - raise LoadError("Missing/invalid value for channel") - - values[channel] = value - - return cls(w=w, id_=id_, name=name, values=values) |