diff --git a/server/src/wled_controller/core/processing/color_strip_stream.py b/server/src/wled_controller/core/processing/color_strip_stream.py index aa804bf..5f0f9e2 100644 --- a/server/src/wled_controller/core/processing/color_strip_stream.py +++ b/server/src/wled_controller/core/processing/color_strip_stream.py @@ -470,6 +470,7 @@ class StaticColorStripStream(ColorStripStream): self._colors_lock = threading.Lock() self._running = False self._thread: Optional[threading.Thread] = None + self._fps = 30 self._update_from_source(source) def _update_from_source(self, source) -> None: @@ -502,12 +503,17 @@ class StaticColorStripStream(ColorStripStream): @property def target_fps(self) -> int: - return 30 + return self._fps @property def led_count(self) -> int: return self._led_count + def set_capture_fps(self, fps: int) -> None: + """Update animation loop rate. Thread-safe (read atomically by the loop).""" + fps = max(1, min(90, fps)) + self._fps = fps + def start(self) -> None: if self._running: return @@ -545,13 +551,12 @@ class StaticColorStripStream(ColorStripStream): logger.info("StaticColorStripStream params updated in-place") def _animate_loop(self) -> None: - """Background thread: compute animated colors at ~30 fps when animation is active. + """Background thread: compute animated colors at target fps when animation is active. Uses double-buffered output arrays (buf_a / buf_b) to avoid per-frame numpy allocations while preserving the identity check used by the processing loop (``colors is prev_colors``). """ - frame_time = 1.0 / 30 # Double-buffer pool — re-allocated only when LED count changes _pool_n = 0 _buf_a = _buf_b = None @@ -560,6 +565,7 @@ class StaticColorStripStream(ColorStripStream): with high_resolution_timer(): while self._running: loop_start = time.perf_counter() + frame_time = 1.0 / self._fps anim = self._animation if anim and anim.get("enabled"): speed = float(anim.get("speed", 1.0)) @@ -645,7 +651,7 @@ class ColorCycleColorStripStream(ColorStripStream): """Color strip stream that smoothly cycles through a user-defined color list. All LEDs receive the same solid color at any moment, continuously interpolating - between the configured colors in a loop at 30 fps. + between the configured colors in a loop. LED count auto-sizes from the connected device when led_count == 0 in the source config; configure(device_led_count) is called by @@ -656,6 +662,7 @@ class ColorCycleColorStripStream(ColorStripStream): self._colors_lock = threading.Lock() self._running = False self._thread: Optional[threading.Thread] = None + self._fps = 30 self._update_from_source(source) def _update_from_source(self, source) -> None: @@ -687,12 +694,17 @@ class ColorCycleColorStripStream(ColorStripStream): @property def target_fps(self) -> int: - return 30 + return self._fps @property def led_count(self) -> int: return self._led_count + def set_capture_fps(self, fps: int) -> None: + """Update animation loop rate. Thread-safe (read atomically by the loop).""" + fps = max(1, min(90, fps)) + self._fps = fps + def start(self) -> None: if self._running: return @@ -729,11 +741,10 @@ class ColorCycleColorStripStream(ColorStripStream): logger.info("ColorCycleColorStripStream params updated in-place") def _animate_loop(self) -> None: - """Background thread: interpolate between colors at ~30 fps. + """Background thread: interpolate between colors at target fps. Uses double-buffered output arrays to avoid per-frame allocations. """ - frame_time = 1.0 / 30 _pool_n = 0 _buf_a = _buf_b = None _use_a = True @@ -741,6 +752,7 @@ class ColorCycleColorStripStream(ColorStripStream): with high_resolution_timer(): while self._running: loop_start = time.perf_counter() + frame_time = 1.0 / self._fps color_list = self._color_list speed = self._cycle_speed n = self._led_count @@ -776,7 +788,7 @@ class GradientColorStripStream(ColorStripStream): """Color strip stream that distributes a gradient across all LEDs. Produces a pre-computed (led_count, 3) uint8 array from user-defined - color stops. When animation is enabled a 30 fps background thread applies + color stops. When animation is enabled a background thread applies dynamic effects (breathing, gradient_shift, wave). LED count auto-sizes from the connected device when led_count == 0 in @@ -788,6 +800,7 @@ class GradientColorStripStream(ColorStripStream): self._colors_lock = threading.Lock() self._running = False self._thread: Optional[threading.Thread] = None + self._fps = 30 self._update_from_source(source) def _update_from_source(self, source) -> None: @@ -816,12 +829,17 @@ class GradientColorStripStream(ColorStripStream): @property def target_fps(self) -> int: - return 30 + return self._fps @property def led_count(self) -> int: return self._led_count + def set_capture_fps(self, fps: int) -> None: + """Update animation loop rate. Thread-safe (read atomically by the loop).""" + fps = max(1, min(90, fps)) + self._fps = fps + def start(self) -> None: if self._running: return @@ -859,12 +877,11 @@ class GradientColorStripStream(ColorStripStream): logger.info("GradientColorStripStream params updated in-place") def _animate_loop(self) -> None: - """Background thread: apply animation effects at ~30 fps when animation is active. + """Background thread: apply animation effects at target fps when animation is active. Uses double-buffered output arrays plus a uint16 scratch buffer for integer-math brightness scaling, avoiding per-frame numpy allocations. """ - frame_time = 1.0 / 30 _cached_base: Optional[np.ndarray] = None _cached_n: int = 0 _cached_stops: Optional[list] = None @@ -876,6 +893,7 @@ class GradientColorStripStream(ColorStripStream): with high_resolution_timer(): while self._running: loop_start = time.perf_counter() + frame_time = 1.0 / self._fps anim = self._animation if anim and anim.get("enabled"): speed = float(anim.get("speed", 1.0))