LedFx/ledfx/effects/__init__.py

271 lines
7.7 KiB
Python

import colorsys
import logging
# from ledfx.effects.audio import FREQUENCY_RANGES
from functools import lru_cache
import numpy as np
import voluptuous as vol
from ledfx.utils import BaseRegistry, RegistryLoader
_LOGGER = logging.getLogger(__name__)
def mix_colors(color_1, color_2, ratio):
if np.array_equal(color_2, []):
return (
color_1[0] * (1 - ratio) + 0,
color_1[1] * (1 - ratio) + 0,
color_1[2] * (1 - ratio) + 0,
)
else:
return (
color_1[0] * (1 - ratio) + color_2[0] * ratio,
color_1[1] * (1 - ratio) + color_2[1] * ratio,
color_1[2] * (1 - ratio) + color_2[2] * ratio,
)
def fill_solid(pixels, color):
pixels[
:,
] = color
def fill_rainbow(pixels, initial_hue, delta_hue):
hue = initial_hue
sat = 0.95
val = 1.0
for i in range(0, len(pixels)):
pixels[i, :] = tuple(
int(i * 255) for i in colorsys.hsv_to_rgb(hue, sat, val)
)
hue = hue + delta_hue
return pixels
def mirror_pixels(pixels):
# TODO: Figure out some better logic here. Needs to reduce the signal
# and reflect across the middle. The prior logic was broken for
# non-uniform effects.
mirror_shape = (np.shape(pixels)[0], 2, np.shape(pixels)[1])
return (
np.append(pixels[::-1], pixels, axis=0)
.reshape(mirror_shape)
.mean(axis=1)
)
def flip_pixels(pixels):
return np.flipud(pixels)
def blur_pixels(pixels, sigma):
rgb_array = pixels.T
rgb_array[0] = smooth(rgb_array[0], sigma)
rgb_array[1] = smooth(rgb_array[1], sigma)
rgb_array[2] = smooth(rgb_array[2], sigma)
return rgb_array.T
def brightness_pixels(pixels, brightness):
pixels = np.multiply(pixels, brightness, out=pixels, casting="unsafe")
return pixels
@lru_cache(maxsize=32)
def _gaussian_kernel1d(sigma, order, radius):
if order < 0:
raise ValueError("order must be non-negative")
p = np.polynomial.Polynomial([0, 0, -0.5 / (sigma * sigma)])
x = np.arange(-radius, radius + 1)
phi_x = np.exp(p(x), dtype=np.double)
phi_x /= phi_x.sum()
if order > 0:
q = np.polynomial.Polynomial([1])
p_deriv = p.deriv()
for _ in range(order):
# f(x) = q(x) * phi(x) = q(x) * exp(p(x))
# f'(x) = (q'(x) + q(x) * p'(x)) * phi(x)
q = q.deriv() + q * p_deriv
phi_x *= q(x)
return phi_x
def smooth(x, sigma):
lw = int(4.0 * float(sigma) + 0.5)
w = _gaussian_kernel1d(sigma, 0, lw)
window_len = len(w)
s = np.r_[x[window_len - 1 : 0 : -1], x, x[-1:-window_len:-1]]
y = np.convolve(w / w.sum(), s, mode="valid")
if window_len < len(x):
return y[(window_len // 2) : -(window_len // 2)]
return y[0 : len(x)]
@BaseRegistry.no_registration
class Effect(BaseRegistry):
"""
Manages an effect
"""
NAME = ""
_pixels = None
_dirty = False
_config = None
_active = False
# Basic effect properties that can be applied to all effects
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(
"blur",
description="Amount to blur the effect",
default=0.0,
): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=10)),
vol.Optional(
"flip", description="Flip the effect", default=False
): bool,
vol.Optional(
"mirror",
description="Mirror the effect",
default=False,
): bool,
vol.Optional(
"brightness",
description="Brightness of strip",
default=1.0,
): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)),
}
)
def __init__(self, ledfx, config):
self._ledfx = ledfx
self._dirty_callback = None
self.update_config(config)
def __del__(self):
if self._active:
self.deactivate()
def activate(self, pixel_count):
"""Attaches an output channel to the effect"""
self._pixels = np.zeros((pixel_count, 3))
self._active = True
_LOGGER.info("Effect {} activated.".format(self.NAME))
def deactivate(self):
"""Detaches an output channel from the effect"""
self._pixels = None
self._active = False
_LOGGER.info("Effect {} deactivated.".format(self.NAME))
def update_config(self, config):
# TODO: Sync locks to ensure everything is thread safe
validated_config = type(self).schema()(config)
self._config = validated_config
def inherited(cls, method):
if hasattr(cls, method) and hasattr(super(cls, cls), method):
return cls.foo == super(cls).foo
return False
# Iterate all the base classes and check to see if there is a custom
# implementation of config updates. If to notify the base class.
valid_classes = list(type(self).__bases__)
valid_classes.append(type(self))
for base in valid_classes:
if base.config_updated != super(base, base).config_updated:
base.config_updated(self, self._config)
_LOGGER.info(
"Effect {} config updated to {}.".format(
self.NAME, validated_config
)
)
def config_updated(self, config):
"""
Optional event for when an effect's config is updated. This
should be used by the subclass only if they need to build up
complex properties off the configuration, otherwise the config
should just be referenced in the effect's loop directly
"""
pass
@property
def is_active(self):
"""Return if the effect is currently active"""
return self._active
def get_pixels(self):
return self.pixels
@property
def pixels(self):
"""Returns the pixels for the channel"""
if not self._active:
raise Exception(
"Attempting to access pixels before effect is active"
)
return np.copy(self._pixels)
@pixels.setter
def pixels(self, pixels):
"""Sets the pixels for the channel"""
if not self._active:
_LOGGER.warning(
"Attempting to set pixels before effect is active. Dropping."
)
return
if isinstance(pixels, tuple):
self._pixels = np.copy(pixels)
elif isinstance(pixels, np.ndarray):
# Apply some of the base output filters if necessary
if self._config["blur"] != 0.0:
pixels = blur_pixels(pixels=pixels, sigma=self._config["blur"])
if self._config["flip"]:
pixels = flip_pixels(pixels)
if self._config["mirror"]:
pixels = mirror_pixels(pixels)
if self._config["brightness"]:
pixels = brightness_pixels(pixels, self._config["brightness"])
self._pixels = np.copy(pixels)
else:
raise TypeError()
self._dirty = True
if self._dirty_callback:
self._dirty_callback()
def setDirtyCallback(self, callback):
self._dirty_callback = callback
@property
def pixel_count(self):
"""Returns the number of pixels for the channel"""
return len(self.pixels)
@property
def name(self):
return self.NAME
class Effects(RegistryLoader):
"""Thin wrapper around the effect registry that manages effects"""
PACKAGE_NAME = "ledfx.effects"
def __init__(self, ledfx):
super().__init__(ledfx=ledfx, cls=Effect, package=self.PACKAGE_NAME)
self._ledfx.audio = None