diff options
author | Ben Connors <benconnors@outlook.com> | 2019-09-26 12:53:53 -0400 |
---|---|---|
committer | Ben Connors <benconnors@outlook.com> | 2019-09-26 12:53:53 -0400 |
commit | 9bd3390071be3db8c366d44e161e828c8263179b (patch) | |
tree | a8a0776b80d727c2a64af9634f0559e1b343f0b0 | |
parent | 2b8a53f98c44e6e78d49b7c246731deef75ed6d3 (diff) |
Finish initial implementation of chasers
- Untested
- (De)serialization functions still need implementing
-rw-r--r-- | blc2/constants.py | 4 | ||||
-rw-r--r-- | blc2/functions/audio.py | 13 | ||||
-rw-r--r-- | blc2/functions/chaser.py | 160 | ||||
-rw-r--r-- | blc2/functions/chaserstep.py | 222 |
4 files changed, 395 insertions, 4 deletions
diff --git a/blc2/constants.py b/blc2/constants.py index 792a877..ad6d174 100644 --- a/blc2/constants.py +++ b/blc2/constants.py @@ -67,3 +67,7 @@ EXTERNAL = "External" INHERIT = "Inherit" MANUAL = "Manual" + +LOOP = "Loop" +RANDOM = "Random" +ONESHOT = "Oneshot" diff --git a/blc2/functions/audio.py b/blc2/functions/audio.py index e41806e..0d59856 100644 --- a/blc2/functions/audio.py +++ b/blc2/functions/audio.py @@ -27,6 +27,7 @@ class Audio(Function): super().__init__(w=w, id_=id_, name=name) self._filename = filename + self._audio_id = self.id if fade_in < 0 or fade_out < 0: raise ValueError("Fades must be nonnegative") @@ -34,10 +35,12 @@ class Audio(Function): self._fade_in = fade_in if filename is not None: - self._duration = self.w.get_audio_length(filename) + self._actual_duration = self.w.get_audio_length(filename) + self._duration = max(0, self._actual_duration - self._fade_out) self._audio_scope = frozenset(((filename,),)) else: self._duration = 0 + self._actual_duration = 0 self._audio_scope = frozenset() @property @@ -76,7 +79,7 @@ class Audio(Function): @property def actual_duration(self): - return self._duration + return self._actual_duration @property def duration(self): @@ -90,7 +93,8 @@ class Audio(Function): library. """ if value != self._duration: - self._duration = value + self._duration = max(value-self._fade_out, 0) + self._actual_duration = value self.w.function_changed(self) def get_data(self): @@ -101,7 +105,7 @@ class Audio(Function): def render(self, t, data = None): return ((), - ((self.id, self._filename, 0, self.fade_in, self.duration, self.fade_out),), + ((self._audio_id, self._filename, 0, self.fade_in, max(0, self.duration-self.fade_out), self.fade_out),), None) @property @@ -120,6 +124,7 @@ class Audio(Function): self.audio_scope = frozenset() self._filename = value + self._audio_id = hash((self._audio_id, self._filename)) self.w.function_changed(self) def serialize(self) -> et.Element: diff --git a/blc2/functions/chaser.py b/blc2/functions/chaser.py new file mode 100644 index 0000000..d17b8f4 --- /dev/null +++ b/blc2/functions/chaser.py @@ -0,0 +1,160 @@ +"""Module for basic chasers.""" + +import random + +from ..constants import CHASER, INFTY, ONESHOT, LOOP, RANDOM +from .function import Function +from .chaserstep import ChaserStep + + +class Chaser(Function): + """Class for chasers.""" + type = CHASER + + def __init__(self, w, id_ = None, name = None, advance_mode = ONESHOT): + super().__init__(w=w, id_=id_, name=name) + + self._steps = [] + self._duration = 0 + self._actual_duration = 0 + self._advance_mode = advance_mode + self._scope = frozenset() + self._audio_scope = frozenset() + + class ChaserData: + """Data for a Chaser render.""" + def __init__(self, chaser, steps=()): + self._chaser = chaser + self.steps = list(steps) + self.audio_id = 0 + + @property + def fade_in(self): + return 0 + + @property + def fade_out(self): + return 0 + + def get_data(self): + return self.ChaserData(self) + + def copy_data(self, data): + return self.ChaserData(self, steps=[i.copy() for i in data.steps]) + + @property + def scope(self): + return self._scope + + @property + def audio_scope(self): + return self._audio_scope + + def _recalculate(self, update=True): + if self._advance_mode != ONESHOT: + self._duration = INFTY + self._actual_duration = INFTY + else: + self._duration = sum((s.duration for s in self.steps)) + self._actual_duration = self._duration + if self._steps: + self._actual_duration += self.steps[-1].actual_duration - self.steps[-1].duration + + scope = set() + ascope = set() + for s in self._steps: + scope.update(s.scope) + ascope.update(s.audio_scope) + self._scope = frozenset(scope) + self._audio_scope = frozenset(ascope) + + if update: + self.w.function_changed(self) + + @property + def actual_duration(self): + return self._actual_duration + + @property + def duration(self): + return self._duration + + @property + def steps(self): + """Return the steps in the chaser.""" + return tuple(self._steps) + + def _function_changed(self, _): + self._recalculate() + + @property + def first_step(self): + """Return the first step number of the chaser.""" + if self._advance_mode == RANDOM: + return random.randint(0, len(self.steps)-1) + return 0 + + def next_steps(self, data): + """Iterate over the next steps in the chaser.""" + n = data.steps[-1].index + if self._advance_mode == RANDOM: + while True: + yield random.randint(0, len(self.steps)-1) + elif self._advance_mode == LOOP: + while True: + n = (n + 1) % len(self.steps) + yield n + else: + return range(n, len(self.steps)) + + def render(self, t, data=None): + if data is None: + data = self.get_data() + if not self._steps: + return (), (), data + elif not data.steps: + if data.audio_id != 0: + raise ValueError("Audio ID must be zero") + n = self.first_step + sd = self.steps[n]._get_data(0, n) #pylint: disable=protected-access + data.steps.append(sd) + + ## Make sure we have all the steps we need + st = data.steps[-1].duration + data.steps[-1].start_time + if st < INFTY and st <= t: + for n in self.next_steps(data): + s = self.steps[n] + st += s.duration + et += s.actual_duration + data.audio_id += 1 + if et >= t: + ## We need this one + data.steps.append(self.steps[n]._get_data(0, n)) #pylint: disable=protected-access + if st > t: + ## We're done + break + + ## Clean up the old steps + data.steps = [i for i in data.steps if i.start_time+i.actual_duration >= t] + if not data.steps: + return (), (), data + + ## Render + lc = {c: 0 for c in self._scope} + ac = [] + for n, d in enumerate(data.steps): + s = d.step + slc, sac, data.steps[n] = s.render(t, data=data.steps[n]) + lc.update(slc) + ac.extend(sac) + + return tuple(lc.items()), ac, data + + def serialize(self): + ## TODO: Implement this + raise NotImplementedError("Not done yet") + + @classmethod + def deserialize(cls, w, e): + ## TODO: Implement this + raise NotImplementedError("Not done yet") diff --git a/blc2/functions/chaserstep.py b/blc2/functions/chaserstep.py new file mode 100644 index 0000000..9884cc6 --- /dev/null +++ b/blc2/functions/chaserstep.py @@ -0,0 +1,222 @@ +"""Module for chaser steps.""" + +from ..constants import CHASERSTEP, EXTERNAL, INHERIT, MANUAL, INFTY +from .function import Function + +class ChaserStep(Function): + """Class representing a single chaser step.""" + type: CHASERSTEP + + def __init__(self, w: "Workspace", id_: int = None, name: str = None, + fade_in = 0, fade_out = 0, function: Function = None, index: int = None, + duration_mode=MANUAL): + super().__init__(w=w, id_=id_, name=name) + self._function = None + self._duration_mode = duration_mode + self._duration = 0 + self._actual_duration = 0 + self._fade_out = fade_out + self._fade_in = fade_in + self._fade_out_mode = EXTERNAL + self._index = index + self._set_function(function, update=False) + + class ChaserStepData: + """Data for a ChaserStep. + + This data is transparent: the interface can be expected not to change + significantly. This is to provide information about the state of the chaser to + managing programs. + + Note that it is an error to modify any attribute of this class: this may mess up + the render call and is not supported in any way. Also, do not call any functions + on this class, regardless of visibility. + + ``end_time`` is relative to t=0, not the start time of this specific step. + """ + @property + def actual_duration(self): + """Return the step's actual duration.""" + return min(self.end_time+self.step.fade_out, self.step.actual_duration) + + def copy(self): + """Duplicate the data.""" + return self.__class__(self.start_time, self.end_time, self.step, self.index, + self.data, self.audio_id) + + def __init__(self, start_time, end_time, step, index, data, audio_id): + self.start_time = start_time + self.end_time = end_time + self.step = step + self.index = index + self.data = data + self.audio_id = audio_id + + @property + def index(self): + """Return this step's index in the chaser.""" + return self._index + + def _set_index(self, value): + """Change this step's index. + + This may only be called from within Chaser. + """ + self._index = value + self.w.function_changed(self) + + @property + def fade_out_mode(self): + """Return the fade out mode.""" + return self._fade_out_mode + + def _recalculate_duration(self, update=True): + if self._duration_mode == INHERIT: + ## Load the duration from the function, if any + if self._function is None: + self._actual_duration = 0 + self._duration = 0 + self._fade_out_mode = EXTERNAL + else: + self._actual_duration = self._function.actual_duration + self._duration = self._function.duration + if self._function.fade_out_mode == EXTERNAL: + self.actual_duration += self._fade_out + self._fade_out_mode = self._function.fade_out_mode + else: + ## Manual duration + self.actual_duration = self.duration + self._fade_out + self._fade_out_mode = EXTERNAL + if update: + self.w.function_changed(self) + + @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") + self._fade_in = v + self._recalculate_duration() + + @property + def fade_out(self): + return self._fade_out + + @fade_out.setter + def fade_out(self, v): + if v < 0: + raise ValueError("Fades must be nonnegative") + self._fade_out = v + self._recalculate_duration() + + @property + def duration_mode(self): + """Return the step's duration mode. + + If the mode is ``INHERIT``, the duration is computed from the duration of the + associated function and the fades. If the mode is ``MANUAL``, the duration is + computed from the manually set duration and the fades. + + Note that ``fade_out_mode`` is inherited from the function when the mode is + ``INHERIT`` and is always ``EXTERNAL`` when manual. + """ + return self._duration_mode + + @duration_mode.setter + def duration_mode(self, v): + if self._duration_mode != v: + self._duration_mode = v + self._recalculate_duration() + + def _function_changed(self, _): + self._recalculate_duration() + + def _function_deleted(self, _): + self._function = None + self._recalculate_duration() + + @property + def function(self): + """Return the step's function.""" + return self._function + + def _set_function(self, v, update=True): + if v != self._function: + if self._function is not None: ## Clear old callbacks + self.w.delete_callbacks(self, self._function.id) + self._function = v + if v is not None: + self.w.register_function_change_callback(v.id, self._function_changed, self) + self.w.register_function_delete_callback(v.id, self._function_deleted, self) + self.fade_out_mode = v.fade_out_mode + self._recalculate_duration(update=update) + + @function.setter + def function(self, v): + self._set_function(v=v, update=True) + + @property + def duration(self): + return self._duration + + @property + def actual_duration(self): + return self._actual_duration + + def get_data(self): + raise TypeError("Cannot retrieve data of a ChaserStep directly") + + def _get_data(self, start_time, audio_id): + fd = None if self._function is None else self._function.get_data() + data = self.ChaserStepData(start_time, INFTY, self, self._index, fd, audio_id) + return data + + def copy_data(self, data): + return data.copy() + + @property + def audio_scope(self): + if self._function is not None: + return self._function.audio_scope + return frozenset() + + @property + def scope(self): + if self._function is not None: + return self._function.scope + return frozenset() + + def render(self, t, data=None): + if self._function is None: + return (), (), data + if data is None: + raise ValueError("Data cannot be None for ChaserStep") + + fade_start = min(data.end_time, self._duration) + t -= data.start_time + + ## Compute lighting multiplier + mul = 1 + if t < self.fade_in: + mul *= t/self.fade_in + if t > fade_start and t <= data.actual_duration: + mul *= 1 - (t-fade_start)/self.fade_out + + ## Render and fade cues + lc, ac, data.data = self._function.render(t, data=data.data) + lc = tuple(((c, int(v*mul)) for c, v in lc)) + ac = tuple(((hash((data.audio_id, aid)), fname, max(self.fade_out, fout), min(fade_start, fstart), min(fout, self.fade_out)) for aid, fname, fin, fstart, fout in ac)) + + return lc, ac, data + + def serialize(self): + ## TODO: Implement this + raise NotImplementedError("Not done yet") + + @classmethod + def deserialize(cls, w, e): + ## TODO: Implement this + raise NotImplementedError("Not done yet") |