Skip to content

Commit

Permalink
Merge branch 'poetry-build' of https://github.com/mariusCZ/LedFx into…
Browse files Browse the repository at this point in the history
… poetry-build
  • Loading branch information
mariusCZ committed Dec 18, 2023
2 parents 71de9bd + a4f5ea9 commit 441e3fa
Show file tree
Hide file tree
Showing 5 changed files with 407 additions and 1 deletion.
15 changes: 15 additions & 0 deletions ledfx/effects/gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ def get_gradient_color(self, point):

return self._gradient_curve[:, int((self.pixel_count - 1) * point)]

def get_gradient_color_vectorized(self, points):
self._assert_gradient()

# Ensure points are within the valid range [0, 1]
points = np.clip(points, 0, 1)

# Calculate indices for the gradient lookup
indices = ((self.pixel_count - 1) * points).astype(int)

# Use advanced indexing to get colors for each point
colors = self._gradient_curve[:, indices]

# Transpose and reshape to get the correct shape (height, width, color_channels)
return colors.transpose(1, 2, 0)

def config_updated(self, config):
"""Invalidate the gradient"""
self._gradient_curve = None
Expand Down
122 changes: 122 additions & 0 deletions ledfx/effects/plasma2d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import logging
import timeit

import numpy as np
import PIL.Image as Image
import voluptuous as vol

from ledfx.effects.gradient import GradientEffect
from ledfx.effects.twod import Twod

_LOGGER = logging.getLogger(__name__)


class Plasma2d(Twod, GradientEffect):
NAME = "Plasma2d"
CATEGORY = "Matrix"
HIDDEN_KEYS = Twod.HIDDEN_KEYS + ["background_color", "gradient_roll"]
ADVANCED_KEYS = Twod.ADVANCED_KEYS + []

_power_funcs = {
"Beat": "beat_power",
"Bass": "bass_power",
"Lows (beat+bass)": "lows_power",
"Mids": "mids_power",
"High": "high_power",
}

start_time = timeit.default_timer()

CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(
"frequency_range",
description="Frequency range for the beat detection",
default="Lows (beat+bass)",
): vol.In(list(_power_funcs.keys())),
vol.Optional(
"v density",
description="Lets pretend its vertical density",
default=0.1,
): vol.All(vol.Coerce(float), vol.Range(min=0.01, max=0.3)),
vol.Optional(
"twist",
description="Like a slice of lemon",
default=0.07,
): vol.All(vol.Coerce(float), vol.Range(min=0.01, max=0.3)),
vol.Optional(
"radius",
description="If you squint its the distance from the center",
default=0.2,
): vol.All(vol.Coerce(float), vol.Range(min=0.01, max=1.0)),
vol.Optional(
"density",
description="kinda how small the plasma is, but who realy knows",
default=0.5,
): vol.All(vol.Coerce(float), vol.Range(min=0.001, max=2.0)),
vol.Optional(
"lower",
description="lower band of density",
default=0.01,
): vol.All(vol.Coerce(float), vol.Range(min=0.01, max=1.0)),
}
)

def __init__(self, ledfx, config):
super().__init__(ledfx, config)

def config_updated(self, config):
self.time = timeit.default_timer()
self.density = self._config["density"]
self.lower = self._config["lower"]
self.power_func = self._power_funcs[self._config["frequency_range"]]
self.v_density = self._config["v density"]
self.twist = self._config["twist"]
self.radius = self.config["radius"]
super().config_updated(config)

def do_once(self):
super().do_once()

def audio_data_updated(self, data):
# Get filtered bar power
self.bar = getattr(data, self.power_func)()

def generate_plasma(self, width, height, time, power):
# Calculate the scale
scale = self.lower + (power * self.density)

# Create coordinate grids with a limited number of steps
y, x = np.ogrid[
0 : min(height, height * scale) : complex(height),
0 : min(width, width * scale) : complex(width),
]

# Calculate the plasma values
plasma = (
np.sin(x * 0.1 + time) * np.cos(y * 0.1 - time)
+ np.sin((x * self.v_density + y * self.twist + time) * 2.5)
+ np.sin(np.sqrt(x**2 + y**2) * self.radius - time)
) * 128 + 128

# Normalize the plasma values to the range [0, 1]
plasma_normalized = (plasma - np.min(plasma)) / (
np.max(plasma) - np.min(plasma)
)
return plasma_normalized

def draw(self):
if self.test:
self.draw_test(self.m_draw)

current_time = timeit.default_timer() - self.start_time

plasma_array = self.generate_plasma(
self.r_width, self.r_height, current_time, self.bar
)

color_mapped_plasma = self.get_gradient_color_vectorized(plasma_array).astype(
np.uint8
)

self.matrix = Image.fromarray(color_mapped_plasma, "RGB")
159 changes: 159 additions & 0 deletions ledfx/effects/plasmawled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import logging
import timeit

import numpy as np
import PIL.Image as Image
import voluptuous as vol

from ledfx.effects.gradient import GradientEffect
from ledfx.effects.twod import Twod

_LOGGER = logging.getLogger(__name__)

# inspired by 2D Hiphotic effect in WLED
# https://github.com/Aircoookie/WLED/blob/main/wled00/FX.cp


class Plasmawled(Twod, GradientEffect):
NAME = "PlasmaWled2d"
CATEGORY = "Matrix"
HIDDEN_KEYS = Twod.HIDDEN_KEYS + ["background_color", "gradient_roll"]
ADVANCED_KEYS = Twod.ADVANCED_KEYS + []

start_time = timeit.default_timer()

_power_funcs = {
"Beat": "beat_power",
"Bass": "bass_power",
"Lows (beat+bass)": "lows_power",
"Mids": "mids_power",
"High": "high_power",
}

CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(
"frequency_range",
description="Frequency range for the beat detection",
default="Lows (beat+bass)",
): vol.In(list(_power_funcs.keys())),
vol.Optional(
"speed",
description="Speed multiplier",
default=128,
): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
vol.Optional(
"h_stretch",
description="Smaller is less block in horizontal dimension",
default=128,
): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
vol.Optional(
"v_stretch",
description="Smaller is less block in vertical dimension",
default=128,
): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
vol.Optional(
"size x",
description="Sound to size multiplier",
default=0.4,
): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)),
vol.Optional(
"speed x",
description="Sound to speed multiplier",
default=0.4,
): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)),
}
)

def __init__(self, ledfx, config):
super().__init__(ledfx, config)

# all trig calculations are in 8 bit space 0 - 255 so lets create lookup tables
self.sine_lookup_table = np.array(
[
(np.sin(theta * (2.0 * np.pi / 255.0)) * 127.5 + 127.5)
for theta in range(256)
],
dtype=int,
)
self.sine_lookup_table = np.clip(self.sine_lookup_table, 0, 255)
self.cosine_lookup_table = np.array(
[
(np.cos(theta * (2.0 * np.pi / 255.0)) * 127.5 + 127.5)
for theta in range(256)
],
dtype=int,
)
self.cosine_lookup_table = np.clip(self.cosine_lookup_table, 0, 255)

def sin8(self, theta):
# Convert theta to a NumPy array of unsigned 8-bit integers for vectorized operations
return self.sine_lookup_table[np.uint8(theta)]

def cos8(self, theta):
# Convert theta to a NumPy array of unsigned 8-bit integers for vectorized operations
return self.cosine_lookup_table[np.uint8(theta)]

def config_updated(self, config):
super().config_updated(config)
self._speed = self._config["speed"]
self.h_stretch = self._config["h_stretch"]
self.v_stretch = self._config["v_stretch"]
self.speedx = self._config["speed x"]
self.sizex = self._config["size x"]
self.power_func = self._power_funcs[self._config["frequency_range"]]
self.speedb = 0
self.sizeb = 0
self.time = 0

def do_once(self):
super().do_once()
# defer things that can't be done when pixel_count is not known

def audio_data_updated(self, data):
self.power = getattr(data, self.power_func)() * 2
self.sizeb = self.power * self.sizex
self.speedb = self.power * self.speedx

def draw(self):
if self.test:
self.draw_test(self.m_draw)

# create data, a numpy array of shape (self.r_width, self.r_height, 1)
data = np.zeros((self.r_height, self.r_width), dtype=np.uint8)

if self.speedx > 0.0:
self.time += self.speedb
time_val = int(self.time * 1000)
else:
time_val = int((timeit.default_timer() - self.start_time) * 1000)

a = time_val / (self._speed + 1)

h_stretch = max(0.01, self.h_stretch - (self.sizeb * self.h_stretch / 3))
v_stretch = max(0.01, self.v_stretch - (self.sizeb * self.v_stretch / 3))

# original python code was as commented below
# kudo's to chatgpt for working through vectorisation
# reduce from cost on 128 x 128 from 40 ms to 1.5 ms

# for x in range(self.r_height):
# for y in range(self.r_width):
# data[x][y] = self.sin8(self.cos8( x * self.h_stretch/16 + a / 3) + self.sin8(y * self.v_stretch/16 + a / 4) + a)

x_indices = np.arange(self.r_height).reshape(-1, 1) # Column vector
y_indices = np.arange(self.r_width) # Row vector

x_vals = x_indices * h_stretch / 16 + a / 3
y_vals = y_indices * v_stretch / 16 + a / 4

# Use vectorized operations to compute indices for lookup tables
sin_cos_indices = (self.cosine_lookup_table[np.uint8(x_vals)] + a) % 256
sin_indices = (self.sine_lookup_table[np.uint8(y_vals)] + a) % 256

# Use advanced indexing to access lookup table values
data = self.sin8(sin_cos_indices + sin_indices) / 255.0

color_mapped_plasma = self.get_gradient_color_vectorized(data).astype(np.uint8)

self.matrix = Image.fromarray(color_mapped_plasma, "RGB")
2 changes: 1 addition & 1 deletion ledfx/effects/twod.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def image_to_pixels(self):
self.matrix = self.matrix.transpose(self.rotate)
if self.matrix.size != (self.t_width, self.t_height):
_LOGGER.error(
f"Image is wrong size {self.matrix.size} vs {self.r_width}x{self.r_height}"
f"Matrix is wrong size {self.matrix.size} vs r {(self.r_width, self.r_height)} vs t {(self.t_width, self.t_height)}"
)

rgb_array = np.frombuffer(self.matrix.tobytes(), dtype=np.uint8)
Expand Down
Loading

0 comments on commit 441e3fa

Please sign in to comment.