initial commit

This commit is contained in:
2026-02-12 00:45:31 -08:00
commit 5f168f370b
3024 changed files with 804889 additions and 0 deletions

View File

@@ -0,0 +1,798 @@
# FastLED Core Library (`src/fl`)
This document introduces the FastLED core library housed under `src/fl/`, first by listing its headers, then by progressively expanding into an educational guide for two audiences:
- Firsttime FastLED users who want to understand what lives below `FastLED.h` and how to use common utilities
- Experienced C++ developers exploring the `fl::` API as a crossplatform, STLfree foundation
FastLED avoids direct dependencies on the C++ standard library in embedded contexts and offers its own STLlike building blocks in the `fl::` namespace.
## Table of Contents
- [Overview and Quick Start](#overview-and-quick-start)
- [Header Groups (5 major areas)](#header-groups-5-major-areas)
- [STLlike data structures and core utilities](#1-stl-like-data-structures-and-core-utilities)
- [Graphics, geometry, and rendering](#2-graphics-geometry-and-rendering)
- [Color, math, and signal processing](#3-color-math-and-signal-processing)
- [Concurrency, async, and functional](#4-concurrency-async-and-functional)
- [I/O, JSON, and text/formatting](#5-io-json-and-textformatting)
- [Comprehensive Module Breakdown](#comprehensive-module-breakdown)
- [Guidance for New Users](#guidance-for-new-users)
- [Guidance for C++ Developers](#guidance-for-c-developers)
---
## Overview and Quick Start
### What is `fl::`?
`fl::` is FastLEDs crossplatform foundation layer. It provides containers, algorithms, memory utilities, math, graphics primitives, async/concurrency, I/O, and platform shims designed to work consistently across embedded targets and host builds. It replaces common `std::` facilities with equivalents tailored for embedded constraints and portability.
Key properties:
- Crossplatform, embeddedfriendly primitives
- Minimal dynamic allocation where possible; clear ownership semantics
- Consistent naming and behavior across compilers/toolchains
- Prefer composable, headerdriven utilities
### Goals and design constraints
- Avoid fragile dependencies on `std::` in embedded builds; prefer `fl::` types
- Emphasize deterministic behavior and low overhead
- Provide familiar concepts (vector, span, optional, variant, function) with embeddedaware implementations
- Offer safe RAII ownership types and moveable wrappers for resource management
- Keep APIs flexible by preferring nonowning views (`fl::span`) as function parameters
### Naming and idioms
- Names live in the `fl::` namespace
- Prefer `fl::span<T>` as input parameters over owning containers
- Use `fl::shared_ptr<T>` / `fl::unique_ptr<T>` instead of raw pointers
- Favor explicit ownership and lifetimes; avoid manual `new`/`delete`
### Getting started: common building blocks
Include the toplevel header you need, or just include `FastLED.h` in sketches. When writing platformagnostic C++ within this repo, include the specific `fl/` headers you use.
Common types to reach for:
- Containers and views: `fl::vector<T>`, `fl::deque<T>`, `fl::span<T>`, `fl::slice<T>`
- Strings and streams: `fl::string`, `fl::ostream`, `fl::sstream`, `fl::printf`
- Optionals and variants: `fl::optional<T>`, `fl::variant<...>`
- Memory/ownership: `fl::unique_ptr<T>`, `fl::shared_ptr<T>`, `fl::weak_ptr<T>`
- Functional: `fl::function<Signature>`, `fl::function_list<Signature>`
- Concurrency: `fl::thread`, `fl::mutex`, `fl::thread_local`
- Async: `fl::promise<T>`, `fl::task`
- Math: `fl::math`, `fl::sin32`, `fl::random`, `fl::gamma`, `fl::gradient`
- Graphics: `fl::raster`, `fl::screenmap`, `fl::rectangular_draw_buffer`, `fl::downscale`, `fl::supersample`
- Color: `fl::hsv`, `fl::hsv16`, `fl::colorutils`
- JSON: `fl::Json` with safe defaults and ergonomic access
Example: using containers, views, and ownership
```cpp
#include "fl/vector.h"
#include "fl/span.h"
#include "fl/memory.h" // for fl::make_unique
void process(fl::span<const int> values) {
// Non-owning view over contiguous data
}
void example() {
fl::vector<int> data;
data.push_back(1);
data.push_back(2);
data.push_back(3);
process(fl::span<const int>(data));
auto ptr = fl::make_unique<int>(42);
// Exclusive ownership; automatically freed when leaving scope
}
```
Example: JSON with safe default access
```cpp
#include "fl/json.h"
void json_example(const fl::string& jsonStr) {
fl::Json json = fl::Json::parse(jsonStr);
int brightness = json["config"]["brightness"] | 128; // default if missing
bool enabled = json["enabled"] | false;
}
```
---
## Header Groups (5 major areas)
### 1) STLlike data structures and core utilities
Containers, views, algorithms, compiletime utilities, memory/ownership, portability helpers.
- Containers: `vector.h`, `deque.h`, `queue.h`, `priority_queue.h`, `set.h`, `map.h`, `unordered_set.h`, `hash_map.h`, `hash_set.h`, `rbtree.h`, `bitset.h`, `bitset_dynamic.h`
- Views and ranges: `span.h`, `slice.h`, `range_access.h`
- Tuples and algebraic types: `tuple.h`, `pair.h`, `optional.h`, `variant.h`
- Algorithms and helpers: `algorithm.h`, `transform.h`, `comparators.h`, `range_access.h`
- Types and traits: `types.h`, `type_traits.h`, `initializer_list.h`, `utility.h`, `move.h`, `template_magic.h`, `stdint.h`, `cstddef.h`, `namespace.h`
- Memory/ownership: `unique_ptr.h`, `shared_ptr.h`, `weak_ptr.h`, `scoped_ptr.h`, `scoped_array.h`, `ptr.h`, `ptr_impl.h`, `referent.h`, `allocator.h`, `memory.h`, `memfill.h`, `inplacenew.h`
- Portability and compiler control: `compiler_control.h`, `force_inline.h`, `virtual_if_not_avr.h`, `has_define.h`, `register.h`, `warn.h`, `trace.h`, `dbg.h`, `assert.h`, `unused.h`, `export.h`, `dll.h`, `deprecated.h`, `avr_disallowed.h`, `bit_cast.h`, `id_tracker.h`, `insert_result.h`, `singleton.h`
Perheader quick descriptions:
- `vector.h`: Dynamically sized contiguous container with embeddedfriendly API.
- `deque.h`: Doubleended queue for efficient front/back operations.
- `queue.h`: FIFO adapter providing push/pop semantics over an underlying container.
- `priority_queue.h`: Heapbased ordered queue for highestpriority retrieval.
- `set.h`: Ordered unique collection with deterministic iteration.
- `map.h`: Ordered keyvalue associative container.
- `unordered_set.h`: Hashbased unique set for average O(1) lookups.
- `hash_map.h`: Hashbased keyvalue container tuned for embedded use.
- `hash_set.h`: Hash set implementation complementing `hash_map.h`.
- `rbtree.h`: Balanced tree primitive used by ordered containers.
- `bitset.h`: Fixedsize compiletime bitset operations.
- `bitset_dynamic.h`: Runtimesized bitset for flexible masks.
- `span.h`: Nonowning view over contiguous memory (preferred function parameter).
- `slice.h`: Strided or subrange view utilities for buffers.
- `range_access.h`: Helpers to unify begin/end access over custom ranges.
- `tuple.h`: Heterogeneous fixedsize aggregate with structured access.
- `pair.h`: Twovalue aggregate type for simple key/value or coordinate pairs.
- `optional.h`: Presence/absence wrapper to avoid sentinel values.
- `variant.h`: Typesafe tagged union for sum types without heap allocation.
- `algorithm.h`: Core algorithms (search, sort helpers, transforms) adapted to `fl::` containers.
- `transform.h`: Functional style elementwise transformations with spans/ranges.
- `comparators.h`: Reusable comparator utilities for ordering operations.
- `types.h`: Canonical type aliases and shared type definitions.
- `type_traits.h`: Compiletime type inspection and enable_ifstyle utilities.
- `initializer_list.h`: Lightweight initializer list support for container construction.
- `utility.h`: Miscellaneous helpers (swap, forward, etc.) suitable for embedded builds.
- `move.h`: Move/forward utilities mirroring standard semantics.
- `template_magic.h`: Metaprogramming helpers to simplify template code.
- `stdint.h`: Fixedwidth integer definitions for crosscompiler consistency.
- `cstddef.h`: Size/ptrdiff and nullptr utilities for portability.
- `namespace.h`: Internal macros/utilities for managing `fl::` namespaces safely.
- `unique_ptr.h`: Exclusive ownership smart pointer with RAII semantics.
- `shared_ptr.h`: Referencecounted shared ownership smart pointer.
- `weak_ptr.h`: Nonowning reference to `shared_ptr`managed objects.
- `scoped_ptr.h`: Scopebound ownership (no move) for simple RAII cleanup.
- `scoped_array.h`: RAII wrapper for array allocations.
- `ptr.h`/`ptr_impl.h`: Pointer abstractions and shared machinery for smart pointers.
- `referent.h`: Base support for referent/observer relationships.
- `allocator.h`: Custom allocators tailored for embedded constraints.
- `memory.h`: Lowlevel memory helpers (construct/destroy, address utilities).
- `memfill.h`: Zerocost fill utilities (prefer over `memset` in codebase).
- `inplacenew.h`: Placement new helpers for manual lifetime management.
- `compiler_control.h`: Unified compiler warning/pragma control macros.
- `force_inline.h`: Portable alwaysinline control macros.
- `virtual_if_not_avr.h`: Virtual specifier abstraction for AVR compatibility.
- `has_define.h`: Preprocessor feature checks and conditional compilation helpers.
- `register.h`: Register annotation shims for portability.
- `warn.h`/`trace.h`/`dbg.h`: Logging, tracing, and diagnostics helpers.
- `assert.h`: Assertions suited for embedded/testing builds.
- `unused.h`: Intentional unused variable/function annotations.
- `export.h`/`dll.h`: Visibility/export macros for shared library boundaries.
- `deprecated.h`: Crosscompiler deprecation annotations.
- `avr_disallowed.h`: Guardrails to prevent unsupported usage on AVR.
- `bit_cast.h`: Safe bit reinterpretation where supported, with fallbacks.
- `id_tracker.h`: ID generation/tracking utility for object registries.
- `insert_result.h`: Standardized result type for associative container inserts.
- `singleton.h`: Simple singleton helper for cases requiring global access.
### 2) Graphics, geometry, and rendering
Rasterization, coordinate mappings, paths, grids, resampling, draw buffers, and related glue.
- Raster and buffers: `raster.h`, `raster_sparse.h`, `rectangular_draw_buffer.h`
- Screen and tiles: `screenmap.h`, `tile2x2.h`
- Coordinates and mappings: `xmap.h`, `xymap.h`, `screenmap.h`
- Paths and traversal: `xypath.h`, `xypath_impls.h`, `xypath_renderer.h`, `traverse_grid.h`, `grid.h`, `line_simplification.h`
- Geometry primitives: `geometry.h`, `point.h`
- Resampling/scaling: `downscale.h`, `upscale.h`, `supersample.h`
- Glue and UI: `leds.h`, `ui.h`, `ui_impl.h`, `rectangular_draw_buffer.h`
- Specialized: `corkscrew.h`, `wave_simulation.h`, `wave_simulation_real.h`, `tile2x2.h`, `screenmap.h`
Perheader quick descriptions:
- `raster.h`: Core raster interface and operations for pixel buffers.
- `raster_sparse.h`: Sparse/partial raster representation for memoryefficient updates.
- `rectangular_draw_buffer.h`: Doublebuffered rectangular draw surface helpers.
- `screenmap.h`: Maps logical coordinates to physical LED indices/layouts.
- `tile2x2.h`: Simple 2×2 tiling utilities for composing larger surfaces.
- `xmap.h`: General coordinate mapping utilities.
- `xymap.h`: XY coordinate to index mapping helpers for matrices and panels.
- `xypath.h`: Path representation in XY space for drawing and effects.
- `xypath_impls.h`: Implementations and algorithms supporting `xypath.h`.
- `xypath_renderer.h`: Renders paths into rasters with configurable styles.
- `traverse_grid.h`: Grid traversal algorithms for curves/lines/fills.
- `grid.h`: Grid data structure and iteration helpers.
- `line_simplification.h`: Path simplification (e.g., DouglasPeuckerstyle) for fewer segments.
- `geometry.h`: Basic geometric computations (distances, intersections, etc.).
- `point.h`: Small coordinate/vector primitive type.
- `downscale.h`: Resampling utilities to reduce resolution while preserving features.
- `upscale.h`: Upsampling utilities for enlarging frames.
- `supersample.h`: Antialiasing via multisample accumulation.
- `leds.h`: Integration helpers bridging LED buffers to rendering utilities.
- `ui.h` / `ui_impl.h`: Minimal UI adapter hooks for demos/tests.
- `corkscrew.h`: Experimental path/trajectory utilities for visual effects.
- `wave_simulation.h` / `wave_simulation_real.h`: Simulated wave dynamics for organic effects.
### 3) Color, math, and signal processing
Color models, gradients, gamma, math helpers, random, noise, mapping, and basic DSP.
- Color and palettes: `colorutils.h`, `colorutils_misc.h`, `hsv.h`, `hsv16.h`, `gradient.h`, `fill.h`, `five_bit_hd_gamma.h`, `gamma.h`
- Math and mapping: `math.h`, `math_macros.h`, `sin32.h`, `map_range.h`, `random.h`, `lut.h`, `clamp.h`, `clear.h`, `splat.h`, `transform.h`
- Noise and waves: `noise_woryley.h`, `wave_simulation.h`, `wave_simulation_real.h`
- DSP and audio: `fft.h`, `fft_impl.h`, `audio.h`, `audio_reactive.h`
- Time utilities: `time.h`, `time_alpha.h`
Perheader quick descriptions:
- `colorutils.h`: Highlevel color operations (blend, scale, lerp) for LED pixels.
- `colorutils_misc.h`: Additional helpers and niche color operations.
- `hsv.h` / `hsv16.h`: HSV color types and conversions (8bit and 16bit variants).
- `gradient.h`: Gradient construction, sampling, and palette utilities.
- `fill.h`: Efficient buffer/palette filling operations for pixel arrays.
- `five_bit_hd_gamma.h`: Gamma correction tables tuned for highdefinition 5bit channels.
- `gamma.h`: Gamma correction functions and LUT helpers.
- `math.h` / `math_macros.h`: Core math primitives/macros for consistent numerics.
- `sin32.h`: Fast fixedpoint sine approximations for animations.
- `map_range.h`: Linear mapping and clamping between numeric ranges.
- `random.h`: Pseudorandom utilities for effects and dithering.
- `lut.h`: Lookup table helpers for precomputed transforms.
- `clamp.h`: Bounds enforcement for numeric types.
- `clear.h`: Clear/zero helpers for buffers with type awareness.
- `splat.h`: Vectorized repeat/write helpers for bulk operations.
- `transform.h`: Element transforms (listed here as it is often used for pixel ops too).
- `noise_woryley.h`: Worley/cellular noise generation utilities.
- `wave_simulation*.h`: Wavefield simulation (also referenced in graphics).
- `fft.h` / `fft_impl.h`: Fast Fourier Transform interfaces and backends.
- `audio.h`: Audio input/stream abstractions for host/platforms that support it.
- `audio_reactive.h`: Utilities to drive visuals from audio features.
- `time.h`: Timekeeping helpers (millis/micros abstractions when available).
- `time_alpha.h`: Smoothed/exponential timebased interpolation helpers.
### 4) Concurrency, async, and functional
Threads, synchronization, async primitives, eventing, and callable utilities.
- Threads and sync: `thread.h`, `mutex.h`, `thread_local.h`
- Async primitives: `promise.h`, `promise_result.h`, `task.h`, `async.h`
- Functional: `function.h`, `function_list.h`, `functional.h`
- Events and engine hooks: `engine_events.h`
Perheader quick descriptions:
- `thread.h`: Portable threading abstraction for supported hosts.
- `mutex.h`: Mutual exclusion primitive compatible with `fl::thread`. Almost all platforms these are fake implementations.
- `thread_local.h`: Threadlocal storage shim for supported compilers.
- `promise.h`: Moveable wrapper around asynchronous result delivery.
- `promise_result.h`: Result type accompanying promises/futures.
- `task.h`: Lightweight async task primitive for orchestration.
- `async.h`: Helpers for async composition and coordination.
- `function.h`: Typeerased callable wrapper analogous to `std::function`.
- `function_list.h`: Multicast list of callables with simple invoke semantics.
- `functional.h`: Adapters, binders, and predicates for composing callables.
- `engine_events.h`: Event channel definitions for enginestyle systems.
### 5) I/O, JSON, and text/formatting
Streams, strings, formatted output, bytestreams, filesystem, JSON.
- Text and streams: `string.h`, `str.h`, `ostream.h`, `istream.h`, `sstream.h`, `strstream.h`, `printf.h`
- JSON: `json.h`
- Bytestreams and I/O: `bytestream.h`, `bytestreammemory.h`, `io.h`, `file_system.h`, `fetch.h`
Perheader quick descriptions:
- `string.h` / `str.h`: String types and helpers without pulling in `std::string`.
- `ostream.h` / `istream.h`: Output/input stream interfaces for host builds.
- `sstream.h` / `strstream.h`: Stringbacked stream buffers and helpers.
- `printf.h`: Small, portable formatted print utilities.
- `json.h`: Safe, ergonomic `fl::Json` API with defaulting operator (`|`).
- `bytestream.h`: Sequential byte I/O abstraction for buffers/streams.
- `bytestreammemory.h`: Inmemory byte stream implementation.
- `io.h`: General I/O helpers for files/streams where available.
- `file_system.h`: Minimal filesystem adapter for host platforms.
- `fetch.h`: Basic fetch/request helpers for networkcapable hosts.
## Comprehensive Module Breakdown
This section groups headers by domain, explains their role, and shows minimal usage snippets. Names shown are representative; see the header list above for the full inventory.
### Containers and Views
- Sequence and associative containers: `vector.h`, `deque.h`, `queue.h`, `priority_queue.h`, `set.h`, `map.h`, `unordered_set.h`, `hash_map.h`, `hash_set.h`, `rbtree.h`
- Nonowning and slicing: `span.h`, `slice.h`, `range_access.h`
Why: Embeddedaware containers with predictable behavior across platforms. Prefer passing `fl::span<T>` to functions.
```cpp
#include "fl/vector.h"
#include "fl/span.h"
size_t count_nonzero(fl::span<const uint8_t> bytes) {
size_t count = 0;
for (uint8_t b : bytes) { if (b != 0) { ++count; } }
return count;
}
```
### Strings and Streams
- Text types and streaming: `string.h`, `str.h`, `ostream.h`, `istream.h`, `sstream.h`, `strstream.h`, `printf.h`
Why: Consistent string/stream facilities without pulling in the standard streams.
```cpp
#include "fl/string.h"
#include "fl/sstream.h"
fl::string greet(const fl::string& name) {
fl::sstream ss;
ss << "Hello, " << name << "!";
return ss.str();
}
```
### Memory and Ownership
- Smart pointers and utilities: `unique_ptr.h`, `shared_ptr.h`, `weak_ptr.h`, `scoped_ptr.h`, `scoped_array.h`, `allocator.h`, `memory.h`, `memfill.h`
Why: RAII ownership with explicit semantics. Prefer `fl::make_shared<T>()`/`fl::make_unique<T>()` patterns where available, or direct constructors provided by these headers.
```cpp
#include "fl/shared_ptr.h"
struct Widget { int value; };
void ownership_example() {
fl::shared_ptr<Widget> w(new Widget{123});
auto w2 = w; // shared ownership
}
```
### Functional Utilities
- Callables and lists: `function.h`, `function_list.h`, `functional.h`
Why: Store callbacks and multicast them safely.
```cpp
#include "fl/function_list.h"
void on_event(int code) { /* ... */ }
void register_handlers() {
fl::function_list<void(int)> handlers;
handlers.add(on_event);
handlers(200); // invoke all
}
```
### Concurrency and Async
- Threads and synchronization: `thread.h`, `mutex.h`, `thread_local.h`
- Async primitives: `promise.h`, `promise_result.h`, `task.h`
Why: Lightweight foundations for parallel work or async orchestration where supported.
```cpp
#include "fl/promise.h"
fl::promise<int> compute_async(); // returns a moveable wrapper around a future-like result
```
### Math, Random, and DSP
- Core math and helpers: `math.h`, `math_macros.h`, `sin32.h`, `random.h`, `map_range.h`
- Color math: `gamma.h`, `gradient.h`, `colorutils.h`, `colorutils_misc.h`, `hsv.h`, `hsv16.h`, `fill.h`
- FFT and analysis: `fft.h`, `fft_impl.h`
Why: Efficient numeric operations for LED effects, audio reactivity, and transforms.
```cpp
#include "fl/gamma.h"
uint8_t apply_gamma(uint8_t v) {
return fl::gamma::correct8(v);
}
```
### Geometry and Grids
- Basic geometry: `point.h`, `geometry.h`
- Grid traversal and simplification: `grid.h`, `traverse_grid.h`, `line_simplification.h`
Why: Building blocks for 2D/3D layouts and path operations.
### Graphics, Rasterization, and Resampling
- Raster and buffers: `raster.h`, `raster_sparse.h`, `rectangular_draw_buffer.h`
- Screen mapping and tiling: `screenmap.h`, `tile2x2.h`, `xmap.h`, `xymap.h`
- Paths and renderers: `xypath.h`, `xypath_impls.h`, `xypath_renderer.h`, `traverse_grid.h`
- Resampling and effects: `downscale.h`, `upscale.h`, `supersample.h`
Why: Efficient frame manipulation for LED matrices and coordinate spaces.
```cpp
#include "fl/downscale.h"
// Downscale a highres buffer to a target raster (API varies by adapter)
```
#### Graphics Deep Dive
This section explains how the major graphics utilities fit together and how to use them effectively for highquality, highperformance rendering on LED strips, matrices, and complex shapes.
- **Wave simulation (1D/2D)**
- Headers: `wave_simulation.h`, `wave_simulation_real.h`
- Concepts:
- Supersampling for quality: choose `SuperSample` factors to run an internal highresolution simulation and downsample for display.
- Consistent speed at higher quality: call `setExtraFrames(u8)` to update the simulation multiple times per frame to maintain perceived speed when supersampling.
- Output accessors: `getf`, `geti16`, `getu8` return float/fixed/byte values; 2D version offers cylindrical wrapping via `setXCylindrical(true)`.
- Tips for “faster” updates:
- Use `setExtraFrames()` to advance multiple internal steps per visual frame without changing your outer timing.
- Prefer `getu8(...)` when feeding color functions or gradients on constrained devices.
- **fl::Leds array and mapped matrix**
- Header: `leds.h`; mapping: `xymap.h`
- `fl::Leds` wraps a `CRGB*` so you can treat it as:
- A plain `CRGB*` via implicit conversion for classic FastLED APIs.
- A 2D surface via `operator()(x, y)` that respects an `XYMap` (serpentine, linebyline, LUT, or custom function).
- Construction:
- `Leds(CRGB* leds, u16 width, u16 height)` for quick serpentine/rectangular usage.
- `Leds(CRGB* leds, const XYMap& xymap)` for full control (serpentine, rectangular, user function, or LUT).
- Access and safety:
- `at(x, y)`/`operator()(x, y)` map to the correct LED index; outofbounds is safe and returns a sentinel.
- `operator[]` exposes rowmajor access when the map is serpentine or linebyline.
- **Matrix mapping (XYMap)**
- Header: `xymap.h`
- Create maps for common layouts:
- `XYMap::constructSerpentine(w, h)` for typical prewired panels.
- `XYMap::constructRectangularGrid(w, h)` for rowmajor matrices.
- `XYMap::constructWithUserFunction(w, h, XYFunction)` for custom wiring.
- `XYMap::constructWithLookUpTable(...)` for arbitrary wiring via LUT.
- Utilities:
- `mapToIndex(x, y)` maps coordinates to strip index.
- `has(x, y)` tests bounds; `toScreenMap()` converts to a float UI mapping.
- **XY paths and path rendering (xypath)**
- Headers: `xypath.h`, `xypath_impls.h`, `xypath_renderer.h`, helpers in `tile2x2.h`, `transform.h`.
- Purpose: parameterized paths in
\([0,1] \to (x,y)\) for drawing lines/curves/shapes with subpixel precision.
- Readymade paths: point, line, circle, heart, Archimedean spiral, rose curves, phyllotaxis, Gielis superformula, and CatmullRom splines with editable control points.
- Rendering:
- Subpixel sampling via `Tile2x2_u8` enables high quality on lowres matrices.
- Use `XYPath::drawColor` or `drawGradient` to rasterize into an `fl::Leds` surface.
- `XYPath::rasterize` writes into a sparse raster for advanced composition.
- Transforms and bounds:
- `setDrawBounds(w, h)` and `setTransform(TransformFloat)` control framing and animation transforms.
- **Subpixel “splat” rendering (Tile2x2)**
- Header: `tile2x2.h`
- Concept: represent a subpixel footprint as a 2×2 tile of coverage values (u8 alphas). When a subpixel position moves between LEDs, neighboring LEDs get proportional contributions.
- Use cases:
- `Tile2x2_u8::draw(color, xymap, out)` to composite with color perpixel.
- Custom blending: `tile.draw(xymap, visitor)` to apply your own alpha/compositing.
- Wrapped tiles: `Tile2x2_u8_wrap` supports cylindrical wrap with interpolation for continuous effects.
- **Downscale**
- Header: `downscale.h`
- Purpose: resample from a higherresolution buffer to a smaller target, preserving features.
- APIs:
- `downscale(src, srcXY, dst, dstXY)` general case.
- `downscaleHalf(...)` optimized 2× reduction (square or mapped) used automatically when sizes match.
- **Upscale**
- Header: `upscale.h`
- Purpose: bilinear upsampling from a lowres buffer to a larger target.
- APIs:
- `upscale(input, output, inW, inH, xyMap)` autoselects optimized paths.
- `upscaleRectangular` and `upscaleRectangularPowerOf2` bypass XY mapping for straight rowmajor layouts.
- Float reference versions exist for validation.
- **Corkscrew (cylindrical projection)**
- Header: `corkscrew.h`
- Goal: draw into a rectangular buffer and project onto a tightly wrapped helical/cylindrical LED layout.
- Inputs and sizing:
- `CorkscrewInput{ totalTurns, numLeds, Gap, invert }` defines geometry; helper `calculateWidth()/calculateHeight()` provide rectangle dimensions for buffer allocation.
- Mapping and iteration:
- `Corkscrew::at_exact(i)` returns the exact position for LED i; `at_wrap(float)` returns a wrapped `Tile2x2_u8_wrap` footprint for subpixelaccurate sampling.
- Use `toScreenMap(diameter)` to produce a `ScreenMap` for UI overlays or browser visualization.
- Rectangular buffer integration:
- `getBuffer()/data()` provide a lazilyinitialized rectangle; `fillBuffer/clearBuffer` manage it.
- `readFrom(source_grid, use_multi_sampling)` projects from a highdef source grid to the corkscrew using multisampling for quality.
- Examples
1) Manual perframe draw (push every frame)
```cpp
#include <FastLED.h>
#include "fl/corkscrew.h"
#include "fl/grid.h"
#include "fl/time.h"
// Your physical LED buffer
constexpr uint16_t NUM_LEDS = 144;
CRGB leds[NUM_LEDS];
// Define a corkscrew geometry (e.g., 19 turns tightly wrapped)
fl::Corkscrew::Input input(/*totalTurns=*/19.0f, /*numLeds=*/NUM_LEDS);
fl::Corkscrew cork(input);
// A simple rectangular source grid to draw into (unwrapped cylinder)
// Match the corkscrew's recommended rectangle
const uint16_t W = cork.cylinder_width();
const uint16_t H = cork.cylinder_height();
fl::Grid<CRGB> rect(W, H);
void draw_pattern(uint32_t t_ms) {
// Example: timeanimated gradient on the unwrapped cylinder
for (uint16_t y = 0; y < H; ++y) {
for (uint16_t x = 0; x < W; ++x) {
uint8_t hue = uint8_t((x * 255u) / (W ? W : 1)) + uint8_t((t_ms / 10) & 0xFF);
rect(x, y) = CHSV(hue, 255, 255);
}
}
}
void project_to_strip() {
// For each LED index i on the physical strip, sample the unwrapped
// rectangle using the subpixel footprint from at_wrap(i)
for (uint16_t i = 0; i < NUM_LEDS; ++i) {
auto tile = cork.at_wrap(float(i));
// tile.at(u,v) gives {absolute_pos, alpha}
// Blend the 2x2 neighborhood from the rectangular buffer
uint16_t r = 0, g = 0, b = 0, a_sum = 0;
for (uint16_t vy = 0; vy < 2; ++vy) {
for (uint16_t vx = 0; vx < 2; ++vx) {
auto entry = tile.at(vx, vy); // pair<vec2i16, u8>
const auto pos = entry.first; // absolute cylinder coords
const uint8_t a = entry.second; // alpha 0..255
if (pos.x >= 0 && pos.x < int(W) && pos.y >= 0 && pos.y < int(H) && a > 0) {
const CRGB& c = rect(uint16_t(pos.x), uint16_t(pos.y));
r += uint16_t(c.r) * a;
g += uint16_t(c.g) * a;
b += uint16_t(c.b) * a;
a_sum += a;
}
}
}
if (a_sum == 0) {
leds[i] = CRGB::Black;
} else {
leds[i].r = uint8_t(r / a_sum);
leds[i].g = uint8_t(g / a_sum);
leds[i].b = uint8_t(b / a_sum);
}
}
}
void setup() {
FastLED.addLeds<WS2812B, 5, GRB>(leds, NUM_LEDS);
}
void loop() {
uint32_t t = fl::time();
draw_pattern(t);
project_to_strip();
FastLED.show();
}
```
2) Automate with task::before_frame (draw right before render)
Use the perframe task API; no direct EngineEvents binding is needed. Perframe tasks are scheduled via `fl::task` and integrated with the frame lifecycle by the async system.
```cpp
#include <FastLED.h>
#include "fl/task.h"
#include "fl/async.h"
#include "fl/corkscrew.h"
#include "fl/grid.h"
constexpr uint16_t NUM_LEDS = 144;
CRGB leds[NUM_LEDS];
fl::Corkscrew cork(fl::Corkscrew::Input(19.0f, NUM_LEDS));
const uint16_t W = cork.cylinder_width();
const uint16_t H = cork.cylinder_height();
fl::Grid<CRGB> rect(W, H);
static void draw_pattern(uint32_t t_ms, fl::Grid<CRGB>& dst);
static void project_to_strip(const fl::Corkscrew& c, const fl::Grid<CRGB>& src, CRGB* out, uint16_t n);
void setup() {
FastLED.addLeds<WS2812B, 5, GRB>(leds, NUM_LEDS);
// Register a before_frame task that runs immediately before each render
fl::task::before_frame().then([&](){
uint32_t t = fl::time();
draw_pattern(t, rect);
project_to_strip(cork, rect, leds, NUM_LEDS);
});
}
void loop() {
// The before_frame task is invoked automatically at the right time
FastLED.show();
// Optionally pump other async work
fl::async_yield();
}
```
- The manual approach gives explicit control each frame.
- The `task::before_frame()` approach schedules work justintime before rendering without manual event wiring. Use `task::after_frame()` for postrender work.
- **Highdefinition HSV16**
- Headers: `hsv16.h`, implementation in `hsv16.cpp`
- `fl::HSV16` stores 16bit H/S/V for highprecision conversion to `CRGB` without banding; construct from `CRGB` or manually, and convert via `ToRGB()` or implicit cast.
- Color boost:
- `HSV16::colorBoost(EaseType saturation_function, EaseType luminance_function)` applies a saturationspace boost similar to gamma, tuned separately for saturation and luminance to counter LED gamut/compression (e.g., WS2812).
- **Easing functions (accurate 8/16bit)**
- Header: `ease.h`
- Accurate easein/out functions with 8 and 16bit variants for quad/cubic/sine families, plus dispatchers: `ease8(type, ...)`, `ease16(type, ...)`.
- Use to shape animation curves, palette traversal, or time alpha outputs.
- **Time alpha**
- Header: `time_alpha.h`
- Helpers for timebased interpolation: `time_alpha8/16/f` compute progress in a window `[start, end]`.
- Stateful helpers:
- `TimeRamp(rise, latch, fall)` for a full riseholdfall cycle.
- `TimeClampedTransition(duration)` for a clamped oneshot.
- Use cases: envelope control for brightness, effect blending, or gating simulation energy over time.
### JSON Utilities
- Safe JSON access: `json.h`
Why: Ergonomic, crashresistant access with defaulting operator (`|`). Prefer the `fl::Json` API for new code.
```cpp
fl::Json cfg = fl::Json::parse("{\"enabled\":true}");
bool enabled = cfg["enabled"] | false;
```
### I/O and Filesystem
- Basic I/O and streams: `io.h`, `ostream.h`, `istream.h`, `sstream.h`, `printf.h`
- Filesystem adapters: `file_system.h`
Why: Crossplatform text formatting, buffered I/O, and optional file access on host builds.
### Algorithms and Utilities
- Algorithms and transforms: `algorithm.h`, `transform.h`, `range_access.h`
- Compiletime utilities: `type_traits.h`, `tuple.h`, `variant.h`, `optional.h`, `utility.h`, `initializer_list.h`, `template_magic.h`, `types.h`, `stdint.h`, `namespace.h`
- Platform shims and control: `compiler_control.h`, `force_inline.h`, `virtual_if_not_avr.h`, `has_define.h`, `register.h`, `warn.h`, `trace.h`, `dbg.h`, `assert.h`, `unused.h`, `export.h`, `dll.h`
Why: Familiar patterns with embeddedappropriate implementations and compilerportable controls.
### Audio and Reactive Systems
### UI System (JSON UI)
FastLED includes a JSONdriven UI layer that can expose controls (sliders, buttons, checkboxes, number fields, dropdowns, titles, descriptions, help, audio visualizers) for interactive demos and remote control.
- **Availability by platform**
- **AVR and other lowmemory chipsets**: disabled by default. The UI is not compiled in on constrained targets.
- **WASM build**: enabled. The web UI is available and connects to the running sketch.
- **Other platforms**: can be enabled if the platform supplies a bridge. See `platforms/ui_defs.h` for the compiletime gate and platform includes.
- **Key headers and switches**
- `platforms/ui_defs.h`: controls `FASTLED_USE_JSON_UI` (defaults to 1 on WASM, 0 elsewhere).
- `fl/ui.h`: C++ UI element classes (UISlider, UIButton, UICheckbox, UINumberField, UIDropdown, UITitle, UIDescription, UIHelp, UIAudio, UIGroup).
- `platforms/shared/ui/json/ui_manager.h`: platformagnostic JSON UI manager that integrates with `EngineEvents`.
- `platforms/shared/ui/json/readme.md`: implementation guide and JSON protocol.
- **Lifecycle and data flow**
- The UI system uses an update manager (`JsonUiManager`) and is integrated with `EngineEvents`:
- On frame lifecycle events, new UI elements are exported as JSON; inbound updates are processed after frames.
- This enables remote control: UI state can be driven locally (browser) or by remote senders issuing JSON updates.
- Send (sketch → UI): when components are added or changed, `JsonUiManager` emits JSON to the platform bridge.
- Receive (UI → sketch): the platform calls `JsonUiManager::updateUiComponents(const char*)` with a JSON object; changes are applied on `onEndFrame()`.
- **Registering handlers to send/receive JSON**
- Platform bridge constructs the manager with a function that forwards JSON to the UI:
- `JsonUiManager(Callback updateJs)` where `updateJs(const char*)` transports JSON to the frontend (WASM example uses JS bindings in `src/platforms/wasm/ui.cpp`).
- To receive UI updates from the frontend, call:
- `MyPlatformUiManager::instance().updateUiComponents(json_str)` to queue changes; `onEndFrame()` applies them.
- **Sketchside usage examples**
Basic setup with controls and groups:
```cpp
#include <FastLED.h>
#include "fl/ui.h"
UISlider brightness("Brightness", 128, 0, 255);
UICheckbox enabled("Enabled", true);
UIButton reset("Reset");
UIDropdown mode("Mode", {fl::string("Rainbow"), fl::string("Waves"), fl::string("Solid")});
UITitle title("Demo Controls");
UIDescription desc("Adjust parameters in real time");
UIHelp help("# Help\nUse the controls to tweak the effect.");
void setup() {
// Group controls visually in the UI
UIGroup group("Main", title, desc, brightness, enabled, mode, reset, help);
// Callbacks are pumped via EngineEvents; they trigger when values change
brightness.onChanged([](UISlider& s){ FastLED.setBrightness((uint8_t)s); });
enabled.onChanged([](UICheckbox& c){ /* toggle effect */ });
mode.onChanged([](UIDropdown& d){ /* change program by d.as_int() */ });
reset.onClicked([](){ /* reset animation state */ });
}
```
Help component (see `examples/UITest/UITest.ino`):
```cpp
UIHelp helpMarkdown(
R"(# FastLED UI\nThis area supports markdown, code blocks, and links.)");
helpMarkdown.setGroup("Documentation");
```
Audio input UI (WASMenabled platforms):
```cpp
UIAudio audio("Mic");
void loop() {
while (audio.hasNext()) {
auto sample = audio.next();
// Use sample data for visualizations
}
FastLED.show();
}
```
- **Platform bridge: sending and receiving JSON**
- Sending from sketch to UI: `JsonUiManager` invokes the `updateJs(const char*)` callback with JSON representing either:
- A JSON array of element definitions (on first export)
- A JSON object of state updates (on changes)
- Receiving from UI to sketch: the platform calls
- `JsonUiManager::updateUiComponents(const char* jsonStr)` where `jsonStr` is a JSON object like:
`{ "id_123": {"value": 200}, "id_456": {"pressed": true } }`
- The manager defers application until the end of the current frame via `onEndFrame()` to ensure consistency.
- **WASM specifics**
- WASM builds provide the web UI and JS glue (see `src/platforms/wasm/ui.cpp` and `src/platforms/wasm/compiler/modules/ui_manager.js`).
- Element definitions may be routed directly to the browser UI manager to avoid feedback loops; updates are logged to an inspector, and inbound messages drive `updateUiComponents`.
- **Enabling on other platforms**
- Implement a platform UI manager using `JsonUiManager` and provide the two weak functions the UI elements call:
- `void addJsonUiComponent(fl::weak_ptr<JsonUiInternal>)`
- `void removeJsonUiComponent(fl::weak_ptr<JsonUiInternal>)`
- Forward platform UI events into `updateUiComponents(jsonStr)`.
- Ensure `EngineEvents` are active so UI updates are exported and applied on frame boundaries.
- Audio adapters and analysis: `audio.h`, `audio_reactive.h`
- Engine hooks: `engine_events.h`
Why: Build effects that respond to input signals.
### Miscellaneous and Specialized
- Wave simulation: `wave_simulation.h`, `wave_simulation_real.h`
- Screen and LED glue: `leds.h`, `screenmap.h`
- Path/visual experimentation: `corkscrew.h`
---
## Guidance for New Users
- If you are writing sketches: include `FastLED.h` and follow examples in `examples/`. The `fl::` headers power those features under the hood.
- If you are extending FastLED internals or building advanced effects: prefer `fl::` containers and `fl::span` over STL equivalents to maintain portability.
- Favor smart pointers and moveable wrappers for resource management. Avoid raw pointers and manual `delete`.
- Use `fl::Json` for robust JSON handling with safe defaults.
## Guidance for C++ Developers
- Treat `fl::` as an embeddedfriendly STL. Many concepts mirror the standard library but are tuned for constrained targets and consistent compiler support.
- When designing APIs, prefer nonowning views (`fl::span<T>`) for input parameters and explicit ownership types for storage.
- Use `compiler_control.h` macros for warning control and portability instead of raw pragmas.
---
This README will evolve alongside the codebase. If you are exploring a specific subsystem, start from the relevant headers listed above and follow includes to supporting modules.

View File

@@ -0,0 +1,15 @@
This directory holds core functionality of FastLED.
Every class/struct/function in this directory must be under the
namespace fl, except for special cases.
This is done because according to the the Arduino build system,
all headers/cpp/hpp files in the root the src directory will be in global
scope and other libraries can #include them by mistake.
This has happened so many times that I've just gone ahead and migrated all
the new code to this directory, to prevent name collisions.
# When to put stuff in `fl` and not `fx`
If it's a visualizer, put it in the `fx` fodler. If it it's something to help to build a visualizer then you put it here.

View File

@@ -0,0 +1,565 @@
#pragma once
#include "fl/type_traits.h"
#include "fl/move.h"
#include "fl/random.h"
namespace fl {
template <typename Iterator>
void reverse(Iterator first, Iterator last) {
while ((first != last) && (first != --last)) {
swap(*first++, *last);
}
}
template <typename Iterator>
Iterator max_element(Iterator first, Iterator last) {
if (first == last) {
return last;
}
Iterator max_iter = first;
++first;
while (first != last) {
if (*max_iter < *first) {
max_iter = first;
}
++first;
}
return max_iter;
}
template <typename Iterator, typename Compare>
Iterator max_element(Iterator first, Iterator last, Compare comp) {
if (first == last) {
return last;
}
Iterator max_iter = first;
++first;
while (first != last) {
if (comp(*max_iter, *first)) {
max_iter = first;
}
++first;
}
return max_iter;
}
template <typename Iterator>
Iterator min_element(Iterator first, Iterator last) {
if (first == last) {
return last;
}
Iterator min_iter = first;
++first;
while (first != last) {
if (*first < *min_iter) {
min_iter = first;
}
++first;
}
return min_iter;
}
template <typename Iterator, typename Compare>
Iterator min_element(Iterator first, Iterator last, Compare comp) {
if (first == last) {
return last;
}
Iterator min_iter = first;
++first;
while (first != last) {
if (comp(*first, *min_iter)) {
min_iter = first;
}
++first;
}
return min_iter;
}
template <typename Iterator1, typename Iterator2>
bool equal(Iterator1 first1, Iterator1 last1, Iterator2 first2) {
while (first1 != last1) {
if (*first1 != *first2) {
return false;
}
++first1;
++first2;
}
return true;
}
template <typename Iterator1, typename Iterator2, typename BinaryPredicate>
bool equal(Iterator1 first1, Iterator1 last1, Iterator2 first2, BinaryPredicate pred) {
while (first1 != last1) {
if (!pred(*first1, *first2)) {
return false;
}
++first1;
++first2;
}
return true;
}
template <typename Iterator1, typename Iterator2>
bool equal(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2) {
while (first1 != last1 && first2 != last2) {
if (*first1 != *first2) {
return false;
}
++first1;
++first2;
}
return first1 == last1 && first2 == last2;
}
template <typename Iterator1, typename Iterator2, typename BinaryPredicate>
bool equal(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2, BinaryPredicate pred) {
while (first1 != last1 && first2 != last2) {
if (!pred(*first1, *first2)) {
return false;
}
++first1;
++first2;
}
return first1 == last1 && first2 == last2;
}
template <typename Container1, typename Container2>
bool equal_container(const Container1& c1, const Container2& c2) {
fl::size size1 = c1.size();
fl::size size2 = c2.size();
if (size1 != size2) {
return false;
}
return equal(c1.begin(), c1.end(), c2.begin(), c2.end());
}
template <typename Container1, typename Container2, typename BinaryPredicate>
bool equal_container(const Container1& c1, const Container2& c2, BinaryPredicate pred) {
fl::size size1 = c1.size();
fl::size size2 = c2.size();
if (size1 != size2) {
return false;
}
return equal(c1.begin(), c1.end(), c2.begin(), c2.end(), pred);
}
template <typename Iterator, typename T>
void fill(Iterator first, Iterator last, const T& value) {
while (first != last) {
*first = value;
++first;
}
}
template <typename Iterator, typename T>
Iterator find(Iterator first, Iterator last, const T& value) {
while (first != last) {
if (*first == value) {
return first;
}
++first;
}
return last;
}
template <typename Iterator, typename UnaryPredicate>
Iterator find_if(Iterator first, Iterator last, UnaryPredicate pred) {
while (first != last) {
if (pred(*first)) {
return first;
}
++first;
}
return last;
}
template <typename Iterator, typename UnaryPredicate>
Iterator find_if_not(Iterator first, Iterator last, UnaryPredicate pred) {
while (first != last) {
if (!pred(*first)) {
return first;
}
++first;
}
return last;
}
template <typename Iterator, typename T>
Iterator remove(Iterator first, Iterator last, const T& value) {
Iterator result = first;
while (first != last) {
if (!(*first == value)) {
if (result != first) {
*result = fl::move(*first);
}
++result;
}
++first;
}
return result;
}
template <typename Iterator, typename UnaryPredicate>
Iterator remove_if(Iterator first, Iterator last, UnaryPredicate pred) {
Iterator result = first;
while (first != last) {
if (!pred(*first)) {
if (result != first) {
*result = fl::move(*first);
}
++result;
}
++first;
}
return result;
}
namespace detail {
// Insertion sort implementation for small arrays
template <typename Iterator, typename Compare>
void insertion_sort(Iterator first, Iterator last, Compare comp) {
if (first == last) return;
for (Iterator i = first + 1; i != last; ++i) {
auto value = fl::move(*i);
Iterator j = i;
while (j != first && comp(value, *(j - 1))) {
*j = fl::move(*(j - 1));
--j;
}
*j = fl::move(value);
}
}
// Median-of-three pivot selection
template <typename Iterator, typename Compare>
Iterator median_of_three(Iterator first, Iterator middle, Iterator last, Compare comp) {
if (comp(*middle, *first)) {
if (comp(*last, *middle)) {
return middle;
} else if (comp(*last, *first)) {
return last;
} else {
return first;
}
} else {
if (comp(*last, *first)) {
return first;
} else if (comp(*last, *middle)) {
return last;
} else {
return middle;
}
}
}
// Partition function for quicksort
template <typename Iterator, typename Compare>
Iterator partition(Iterator first, Iterator last, Compare comp) {
Iterator middle = first + (last - first) / 2;
Iterator pivot_iter = median_of_three(first, middle, last - 1, comp);
// Move pivot to end
swap(*pivot_iter, *(last - 1));
Iterator pivot = last - 1;
Iterator i = first;
for (Iterator j = first; j != pivot; ++j) {
if (comp(*j, *pivot)) {
swap(*i, *j);
++i;
}
}
swap(*i, *pivot);
return i;
}
// Heapsort implementation (fallback for deep recursion)
template <typename Iterator, typename Compare>
void sift_down(Iterator first, Iterator start, Iterator end, Compare comp) {
Iterator root = start;
while (root - first <= (end - first - 2) / 2) {
Iterator child = first + 2 * (root - first) + 1;
Iterator swap_iter = root;
if (comp(*swap_iter, *child)) {
swap_iter = child;
}
if (child + 1 <= end && comp(*swap_iter, *(child + 1))) {
swap_iter = child + 1;
}
if (swap_iter == root) {
return;
} else {
swap(*root, *swap_iter);
root = swap_iter;
}
}
}
template <typename Iterator, typename Compare>
void heapify(Iterator first, Iterator last, Compare comp) {
Iterator start = first + (last - first - 2) / 2;
while (true) {
sift_down(first, start, last - 1, comp);
if (start == first) {
break;
}
--start;
}
}
template <typename Iterator, typename Compare>
void heap_sort(Iterator first, Iterator last, Compare comp) {
heapify(first, last, comp);
Iterator end = last - 1;
while (end > first) {
swap(*end, *first);
sift_down(first, first, end - 1, comp);
--end;
}
}
// Quicksort implementation
template <typename Iterator, typename Compare>
void quicksort_impl(Iterator first, Iterator last, Compare comp) {
if (last - first <= 16) { // Use insertion sort for small arrays
insertion_sort(first, last, comp);
return;
}
Iterator pivot = partition(first, last, comp);
quicksort_impl(first, pivot, comp);
quicksort_impl(pivot + 1, last, comp);
}
// Rotate elements in range [first, last) so that middle becomes the new first
template <typename Iterator>
void rotate_impl(Iterator first, Iterator middle, Iterator last) {
if (first == middle || middle == last) {
return;
}
Iterator next = middle;
while (first != next) {
swap(*first++, *next++);
if (next == last) {
next = middle;
} else if (first == middle) {
middle = next;
}
}
}
// Find the position where value should be inserted in sorted range [first, last)
template <typename Iterator, typename T, typename Compare>
Iterator lower_bound_impl(Iterator first, Iterator last, const T& value, Compare comp) {
auto count = last - first;
while (count > 0) {
auto step = count / 2;
Iterator it = first + step;
if (comp(*it, value)) {
first = ++it;
count -= step + 1;
} else {
count = step;
}
}
return first;
}
// In-place merge operation for merge sort (stable sort)
template <typename Iterator, typename Compare>
void merge_inplace(Iterator first, Iterator middle, Iterator last, Compare comp) {
// If one of the ranges is empty, nothing to merge
if (first == middle || middle == last) {
return;
}
// If arrays are small enough, use insertion-based merge
auto left_size = middle - first;
auto right_size = last - middle;
if (left_size + right_size <= 32) {
// Simple insertion-based merge for small arrays
Iterator left = first;
Iterator right = middle;
while (left < middle && right < last) {
if (!comp(*right, *left)) {
// left element is in correct position
++left;
} else {
// right element needs to be inserted into left part
auto value = fl::move(*right);
Iterator shift_end = right;
Iterator shift_start = left;
// Shift elements to make room
while (shift_end > shift_start) {
*shift_end = fl::move(*(shift_end - 1));
--shift_end;
}
*left = fl::move(value);
++left;
++middle; // middle has shifted right
++right;
}
}
return;
}
// For larger arrays, use rotation-based merge
if (left_size == 0 || right_size == 0) {
return;
}
if (left_size == 1) {
// Find insertion point for the single left element in right array
Iterator pos = lower_bound_impl(middle, last, *first, comp);
rotate_impl(first, middle, pos);
return;
}
if (right_size == 1) {
// Find insertion point for the single right element in left array
Iterator pos = lower_bound_impl(first, middle, *(last - 1), comp);
rotate_impl(pos, middle, last);
return;
}
// Divide both arrays and recursively merge
Iterator left_mid = first + left_size / 2;
Iterator right_mid = lower_bound_impl(middle, last, *left_mid, comp);
// Rotate to bring the two middle parts together
rotate_impl(left_mid, middle, right_mid);
// Update middle position
Iterator new_middle = left_mid + (right_mid - middle);
// Recursively merge the two parts
merge_inplace(first, left_mid, new_middle, comp);
merge_inplace(new_middle, right_mid, last, comp);
}
// Merge sort implementation (stable, in-place)
template <typename Iterator, typename Compare>
void mergesort_impl(Iterator first, Iterator last, Compare comp) {
auto size = last - first;
if (size <= 16) { // Use insertion sort for small arrays (it's stable)
insertion_sort(first, last, comp);
return;
}
Iterator middle = first + size / 2;
mergesort_impl(first, middle, comp);
mergesort_impl(middle, last, comp);
merge_inplace(first, middle, last, comp);
}
} // namespace detail
// Sort function with custom comparator (using quicksort)
template <typename Iterator, typename Compare>
void sort(Iterator first, Iterator last, Compare comp) {
if (first == last || first + 1 == last) {
return; // Already sorted or empty
}
detail::quicksort_impl(first, last, comp);
}
// Sort function with default comparator
template <typename Iterator>
void sort(Iterator first, Iterator last) {
// Use explicit template parameter to avoid C++14 auto in lambda
typedef typename fl::remove_reference<decltype(*first)>::type value_type;
sort(first, last, [](const value_type& a, const value_type& b) { return a < b; });
}
// Stable sort function with custom comparator (using merge sort)
template <typename Iterator, typename Compare>
void stable_sort(Iterator first, Iterator last, Compare comp) {
if (first == last || first + 1 == last) {
return; // Already sorted or empty
}
detail::mergesort_impl(first, last, comp);
}
// Stable sort function with default comparator
template <typename Iterator>
void stable_sort(Iterator first, Iterator last) {
// Use explicit template parameter to avoid C++14 auto in lambda
typedef typename fl::remove_reference<decltype(*first)>::type value_type;
stable_sort(first, last, [](const value_type& a, const value_type& b) { return a < b; });
}
// Shuffle function with custom random generator (Fisher-Yates shuffle)
template <typename Iterator, typename RandomGenerator>
void shuffle(Iterator first, Iterator last, RandomGenerator& g) {
if (first == last) {
return; // Empty range, nothing to shuffle
}
auto n = last - first;
for (auto i = n - 1; i > 0; --i) {
// Generate random index from 0 to i (inclusive)
auto j = g() % (i + 1);
// Swap elements at positions i and j
swap(*(first + i), *(first + j));
}
}
// Shuffle function with fl::fl_random instance
template <typename Iterator>
void shuffle(Iterator first, Iterator last, fl_random& rng) {
if (first == last) {
return; // Empty range, nothing to shuffle
}
auto n = last - first;
for (auto i = n - 1; i > 0; --i) {
// Generate random index from 0 to i (inclusive)
auto j = rng(static_cast<u32>(i + 1));
// Swap elements at positions i and j
swap(*(first + i), *(first + j));
}
}
// Shuffle function with default random generator
template <typename Iterator>
void shuffle(Iterator first, Iterator last) {
shuffle(first, last, default_random());
}
} // namespace fl

View File

@@ -0,0 +1,18 @@
#pragma once
#if defined(FASTLED_TESTING) || defined(__EMSCRIPTEN__)
// max_align_t and alignof
#include <cstddef> // ok include
#endif
#ifdef __EMSCRIPTEN__
#define FL_ALIGN_BYTES 8
#define FL_ALIGN alignas(FL_ALIGN_BYTES)
#define FL_ALIGN_AS(T) alignas(alignof(T))
#else
#define FL_ALIGN_BYTES 1
#define FL_ALIGN
#define FL_ALIGN_AS(T)
#endif

View File

@@ -0,0 +1,138 @@
#include <stdlib.h>
#include "fl/allocator.h"
#include "fl/namespace.h"
#include "fl/int.h"
#include "fl/thread_local.h"
#ifdef ESP32
#include "esp_heap_caps.h"
#include "esp_system.h"
#endif
namespace fl {
namespace {
#ifdef ESP32
// On esp32, attempt to always allocate in psram first.
void *DefaultAlloc(fl::size size) {
void *out = heap_caps_malloc(size, MALLOC_CAP_SPIRAM);
if (out == nullptr) {
// Fallback to default allocator.
out = heap_caps_malloc(size, MALLOC_CAP_DEFAULT);
}
return out;
}
void DefaultFree(void *ptr) { heap_caps_free(ptr); }
#else
void *DefaultAlloc(fl::size size) { return malloc(size); }
void DefaultFree(void *ptr) { free(ptr); }
#endif
void *(*Alloc)(fl::size) = DefaultAlloc;
void (*Dealloc)(void *) = DefaultFree;
#if defined(FASTLED_TESTING)
// Test hook interface pointer
MallocFreeHook* gMallocFreeHook = nullptr;
int& tls_reintrancy_count() {
static fl::ThreadLocal<int> enabled;
return enabled.access();
}
struct MemoryGuard {
int& reintrancy_count;
MemoryGuard(): reintrancy_count(tls_reintrancy_count()) {
reintrancy_count++;
}
~MemoryGuard() {
reintrancy_count--;
}
bool enabled() const {
return reintrancy_count <= 1;
}
};
#endif
} // namespace
#if defined(FASTLED_TESTING)
void SetMallocFreeHook(MallocFreeHook* hook) {
gMallocFreeHook = hook;
}
void ClearMallocFreeHook() {
gMallocFreeHook = nullptr;
}
#endif
void SetPSRamAllocator(void *(*alloc)(fl::size), void (*free)(void *)) {
Alloc = alloc;
Dealloc = free;
}
void *PSRamAllocate(fl::size size, bool zero) {
void *ptr = Alloc(size);
if (ptr && zero) {
memset(ptr, 0, size);
}
#if defined(FASTLED_TESTING)
if (gMallocFreeHook && ptr) {
MemoryGuard allows_hook;
if (allows_hook.enabled()) {
gMallocFreeHook->onMalloc(ptr, size);
}
}
#endif
return ptr;
}
void PSRamDeallocate(void *ptr) {
#if defined(FASTLED_TESTING)
if (gMallocFreeHook && ptr) {
// gMallocFreeHook->onFree(ptr);
MemoryGuard allows_hook;
if (allows_hook.enabled()) {
gMallocFreeHook->onFree(ptr);
}
}
#endif
Dealloc(ptr);
}
void* Malloc(fl::size size) {
void* ptr = Alloc(size);
#if defined(FASTLED_TESTING)
if (gMallocFreeHook && ptr) {
MemoryGuard allows_hook;
if (allows_hook.enabled()) {
gMallocFreeHook->onMalloc(ptr, size);
}
}
#endif
return ptr;
}
void Free(void *ptr) {
#if defined(FASTLED_TESTING)
if (gMallocFreeHook && ptr) {
MemoryGuard allows_hook;
if (allows_hook.enabled()) {
gMallocFreeHook->onFree(ptr);
}
}
#endif
Dealloc(ptr);
}
} // namespace fl

View File

@@ -0,0 +1,774 @@
#pragma once
#include <stdlib.h>
#include <string.h>
#include "fl/inplacenew.h"
#include "fl/memfill.h"
#include "fl/type_traits.h"
#include "fl/unused.h"
#include "fl/bit_cast.h"
#include "fl/stdint.h"
#include "fl/bitset.h"
#ifndef FASTLED_DEFAULT_SLAB_SIZE
#define FASTLED_DEFAULT_SLAB_SIZE 8
#endif
namespace fl {
// Test hooks for malloc/free operations
#if defined(FASTLED_TESTING)
// Interface class for malloc/free test hooks
class MallocFreeHook {
public:
virtual ~MallocFreeHook() = default;
virtual void onMalloc(void* ptr, fl::size size) = 0;
virtual void onFree(void* ptr) = 0;
};
// Set test hooks for malloc and free operations
void SetMallocFreeHook(MallocFreeHook* hook);
// Clear test hooks (set to nullptr)
void ClearMallocFreeHook();
#endif
void SetPSRamAllocator(void *(*alloc)(fl::size), void (*free)(void *));
void *PSRamAllocate(fl::size size, bool zero = true);
void PSRamDeallocate(void *ptr);
void* Malloc(fl::size size);
void Free(void *ptr);
template <typename T> class PSRamAllocator {
public:
static T *Alloc(fl::size n) {
void *ptr = PSRamAllocate(sizeof(T) * n, true);
return fl::bit_cast_ptr<T>(ptr);
}
static void Free(T *p) {
if (p == nullptr) {
return;
}
PSRamDeallocate(p);
}
};
// std compatible allocator.
template <typename T> class allocator {
public:
// Type definitions required by STL
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = fl::size;
using difference_type = ptrdiff_t;
// Rebind allocator to type U
template <typename U>
struct rebind {
using other = allocator<U>;
};
// Default constructor
allocator() noexcept {}
// Copy constructor
template <typename U>
allocator(const allocator<U>&) noexcept {}
// Destructor
~allocator() noexcept {}
// Use this to allocate large blocks of memory for T.
// This is useful for large arrays or objects that need to be allocated
// in a single block.
T* allocate(fl::size n) {
if (n == 0) {
return nullptr; // Handle zero allocation
}
fl::size size = sizeof(T) * n;
void *ptr = Malloc(size);
if (ptr == nullptr) {
return nullptr; // Handle allocation failure
}
fl::memfill(ptr, 0, sizeof(T) * n); // Zero-initialize the memory
return static_cast<T*>(ptr);
}
void deallocate(T* p, fl::size n) {
FASTLED_UNUSED(n);
if (p == nullptr) {
return; // Handle null pointer
}
Free(p); // Free the allocated memory
}
// Construct an object at the specified address
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
if (p == nullptr) return;
new(static_cast<void*>(p)) U(fl::forward<Args>(args)...);
}
// Destroy an object at the specified address
template <typename U>
void destroy(U* p) {
if (p == nullptr) return;
p->~U();
}
};
template <typename T> class allocator_psram {
public:
// Type definitions required by STL
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = fl::size;
using difference_type = ptrdiff_t;
// Rebind allocator to type U
template <typename U>
struct rebind {
using other = allocator_psram<U>;
};
// Default constructor
allocator_psram() noexcept {}
// Copy constructor
template <typename U>
allocator_psram(const allocator_psram<U>&) noexcept {}
// Destructor
~allocator_psram() noexcept {}
// Allocate memory for n objects of type T
T* allocate(fl::size n) {
return PSRamAllocator<T>::Alloc(n);
}
// Deallocate memory for n objects of type T
void deallocate(T* p, fl::size n) {
PSRamAllocator<T>::Free(p);
FASTLED_UNUSED(n);
}
// Construct an object at the specified address
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
if (p == nullptr) return;
new(static_cast<void*>(p)) U(fl::forward<Args>(args)...);
}
// Destroy an object at the specified address
template <typename U>
void destroy(U* p) {
if (p == nullptr) return;
p->~U();
}
};
// Slab allocator for fixed-size objects
// Optimized for frequent allocation/deallocation of objects of the same size
// Uses pre-allocated memory slabs with free lists to reduce fragmentation
template <typename T, fl::size SLAB_SIZE = FASTLED_DEFAULT_SLAB_SIZE>
class SlabAllocator {
private:
static constexpr fl::size SLAB_BLOCK_SIZE = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
static constexpr fl::size BLOCKS_PER_SLAB = SLAB_SIZE;
static constexpr fl::size SLAB_MEMORY_SIZE = SLAB_BLOCK_SIZE * BLOCKS_PER_SLAB;
struct Slab {
Slab* next;
u8* memory;
fl::size allocated_count;
fl::bitset_fixed<BLOCKS_PER_SLAB> allocated_blocks; // Track which blocks are allocated
Slab() : next(nullptr), memory(nullptr), allocated_count(0) {}
~Slab() {
if (memory) {
free(memory);
}
}
};
Slab* slabs_;
fl::size total_allocated_;
fl::size total_deallocated_;
Slab* createSlab() {
Slab* slab = static_cast<Slab*>(malloc(sizeof(Slab)));
if (!slab) {
return nullptr;
}
// Use placement new to properly initialize the Slab
new(slab) Slab();
slab->memory = static_cast<u8*>(malloc(SLAB_MEMORY_SIZE));
if (!slab->memory) {
slab->~Slab();
free(slab);
return nullptr;
}
// Initialize all blocks in the slab as free
slab->allocated_blocks.reset(); // All blocks start as free
// Add slab to the slab list
slab->next = slabs_;
slabs_ = slab;
return slab;
}
void* allocateFromSlab(fl::size n = 1) {
// Try to find n contiguous free blocks in existing slabs
for (Slab* slab = slabs_; slab; slab = slab->next) {
void* ptr = findContiguousBlocks(slab, n);
if (ptr) {
return ptr;
}
}
// No contiguous blocks found, create new slab if n fits
if (n <= BLOCKS_PER_SLAB) {
if (!createSlab()) {
return nullptr; // Out of memory
}
// Try again with the new slab
return findContiguousBlocks(slabs_, n);
}
// Request too large for slab, fall back to malloc
return nullptr;
}
void* findContiguousBlocks(Slab* slab, fl::size n) {
// Check if allocation is too large for this slab
if (n > BLOCKS_PER_SLAB) {
return nullptr;
}
// Use bitset's find_run to find n contiguous free blocks (false = free)
fl::i32 start = slab->allocated_blocks.find_run(false, static_cast<fl::u32>(n));
if (start >= 0) {
// Mark blocks as allocated
for (fl::size i = 0; i < n; ++i) {
slab->allocated_blocks.set(static_cast<fl::u32>(start + i), true);
}
slab->allocated_count += n;
total_allocated_ += n;
// Return pointer to the first block
return slab->memory + static_cast<fl::size>(start) * SLAB_BLOCK_SIZE;
}
return nullptr;
}
void deallocateToSlab(void* ptr, fl::size n = 1) {
if (!ptr) {
return;
}
// Find which slab this block belongs to
for (Slab* slab = slabs_; slab; slab = slab->next) {
u8* slab_start = slab->memory;
u8* slab_end = slab_start + SLAB_MEMORY_SIZE;
u8* block_ptr = fl::bit_cast_ptr<u8>(ptr);
if (block_ptr >= slab_start && block_ptr < slab_end) {
fl::size block_index = (block_ptr - slab_start) / SLAB_BLOCK_SIZE;
// Mark blocks as free in the bitset
for (fl::size i = 0; i < n; ++i) {
if (block_index + i < BLOCKS_PER_SLAB) {
slab->allocated_blocks.set(block_index + i, false);
}
}
slab->allocated_count -= n;
total_deallocated_ += n;
break;
}
}
}
public:
// Constructor
SlabAllocator() : slabs_(nullptr), total_allocated_(0), total_deallocated_(0) {}
// Destructor
~SlabAllocator() {
cleanup();
}
// Non-copyable
SlabAllocator(const SlabAllocator&) = delete;
SlabAllocator& operator=(const SlabAllocator&) = delete;
// Movable
SlabAllocator(SlabAllocator&& other) noexcept
: slabs_(other.slabs_), total_allocated_(other.total_allocated_), total_deallocated_(other.total_deallocated_) {
other.slabs_ = nullptr;
other.total_allocated_ = 0;
other.total_deallocated_ = 0;
}
SlabAllocator& operator=(SlabAllocator&& other) noexcept {
if (this != &other) {
cleanup();
slabs_ = other.slabs_;
total_allocated_ = other.total_allocated_;
total_deallocated_ = other.total_deallocated_;
other.slabs_ = nullptr;
other.total_allocated_ = 0;
other.total_deallocated_ = 0;
}
return *this;
}
T* allocate(fl::size n = 1) {
if (n == 0) {
return nullptr;
}
// Try to allocate from slab first
void* ptr = allocateFromSlab(n);
if (ptr) {
fl::memfill(ptr, 0, sizeof(T) * n);
return static_cast<T*>(ptr);
}
// Fall back to regular malloc for large allocations
ptr = malloc(sizeof(T) * n);
if (ptr) {
fl::memfill(ptr, 0, sizeof(T) * n);
}
return static_cast<T*>(ptr);
}
void deallocate(T* ptr, fl::size n = 1) {
if (!ptr) {
return;
}
// Try to deallocate from slab first
bool found_in_slab = false;
for (Slab* slab = slabs_; slab; slab = slab->next) {
u8* slab_start = slab->memory;
u8* slab_end = slab_start + SLAB_MEMORY_SIZE;
u8* block_ptr = fl::bit_cast_ptr<u8>(static_cast<void*>(ptr));
if (block_ptr >= slab_start && block_ptr < slab_end) {
deallocateToSlab(ptr, n);
found_in_slab = true;
break;
}
}
if (!found_in_slab) {
// This was allocated with regular malloc
free(ptr);
}
}
// Get allocation statistics
fl::size getTotalAllocated() const { return total_allocated_; }
fl::size getTotalDeallocated() const { return total_deallocated_; }
fl::size getActiveAllocations() const { return total_allocated_ - total_deallocated_; }
// Get number of slabs
fl::size getSlabCount() const {
fl::size count = 0;
for (Slab* slab = slabs_; slab; slab = slab->next) {
++count;
}
return count;
}
// Cleanup all slabs
void cleanup() {
while (slabs_) {
Slab* next = slabs_->next;
slabs_->~Slab();
free(slabs_);
slabs_ = next;
}
total_allocated_ = 0;
total_deallocated_ = 0;
}
};
// STL-compatible slab allocator
template <typename T, fl::size SLAB_SIZE = FASTLED_DEFAULT_SLAB_SIZE>
class allocator_slab {
public:
// Type definitions required by STL
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = fl::size;
using difference_type = ptrdiff_t;
// Rebind allocator to type U
template <typename U>
struct rebind {
using other = typename fl::conditional<
fl::is_same<U, void>::value,
allocator_slab<char, SLAB_SIZE>,
allocator_slab<U, SLAB_SIZE>
>::type;
};
// Default constructor
allocator_slab() noexcept {}
// Copy constructor
allocator_slab(const allocator_slab& other) noexcept {
FASTLED_UNUSED(other);
}
// Copy assignment
allocator_slab& operator=(const allocator_slab& other) noexcept {
FASTLED_UNUSED(other);
return *this;
}
// Template copy constructor
template <typename U>
allocator_slab(const allocator_slab<U, SLAB_SIZE>& other) noexcept {
FASTLED_UNUSED(other);
}
// Destructor
~allocator_slab() noexcept {}
private:
// Get the shared static allocator instance
static SlabAllocator<T, SLAB_SIZE>& get_allocator() {
static SlabAllocator<T, SLAB_SIZE> allocator;
return allocator;
}
public:
// Allocate memory for n objects of type T
T* allocate(fl::size n) {
// Use a static allocator instance per type/size combination
SlabAllocator<T, SLAB_SIZE>& allocator = get_allocator();
return allocator.allocate(n);
}
// Deallocate memory for n objects of type T
void deallocate(T* p, fl::size n) {
// Use the same static allocator instance
SlabAllocator<T, SLAB_SIZE>& allocator = get_allocator();
allocator.deallocate(p, n);
}
// Construct an object at the specified address
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
if (p == nullptr) return;
new(static_cast<void*>(p)) U(fl::forward<Args>(args)...);
}
// Destroy an object at the specified address
template <typename U>
void destroy(U* p) {
if (p == nullptr) return;
p->~U();
}
// Cleanup method to clean up the static slab allocator
void cleanup() {
// Access the same static allocator instance and clean it up
static SlabAllocator<T, SLAB_SIZE> allocator;
allocator.cleanup();
}
// Equality comparison
bool operator==(const allocator_slab& other) const noexcept {
FASTLED_UNUSED(other);
return true; // All instances are equivalent
}
bool operator!=(const allocator_slab& other) const noexcept {
return !(*this == other);
}
};
// Inlined allocator that stores the first N elements inline
// Falls back to the base allocator for additional elements
template <typename T, fl::size N, typename BaseAllocator = fl::allocator<T>>
class allocator_inlined {
private:
// Inlined storage block
struct InlinedStorage {
alignas(T) u8 data[N * sizeof(T)];
InlinedStorage() {
fl::memfill(data, 0, sizeof(data));
}
};
InlinedStorage m_inlined_storage;
BaseAllocator m_base_allocator;
fl::size m_inlined_used = 0;
fl::bitset_fixed<N> m_free_bits; // Track free slots for inlined memory only
fl::size m_active_allocations = 0; // Track current active allocations
public:
// Type definitions required by STL
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = fl::size;
using difference_type = ptrdiff_t;
// Rebind allocator to type U
template <typename U>
struct rebind {
using other = allocator_inlined<U, N, typename BaseAllocator::template rebind<U>::other>;
};
// Default constructor
allocator_inlined() noexcept = default;
// Copy constructor
allocator_inlined(const allocator_inlined& other) noexcept {
// Copy inlined data
m_inlined_used = other.m_inlined_used;
for (fl::size i = 0; i < m_inlined_used; ++i) {
new (&get_inlined_ptr()[i]) T(other.get_inlined_ptr()[i]);
}
// Copy free bits
m_free_bits = other.m_free_bits;
// Note: Heap allocations are not copied, only inlined data
// Copy active allocations count
m_active_allocations = other.m_active_allocations;
}
// Copy assignment
allocator_inlined& operator=(const allocator_inlined& other) noexcept {
if (this != &other) {
clear();
// Copy inlined data
m_inlined_used = other.m_inlined_used;
for (fl::size i = 0; i < m_inlined_used; ++i) {
new (&get_inlined_ptr()[i]) T(other.get_inlined_ptr()[i]);
}
// Copy free bits
m_free_bits = other.m_free_bits;
// Note: Heap allocations are not copied, only inlined data
// Copy active allocations count
m_active_allocations = other.m_active_allocations;
}
return *this;
}
// Template copy constructor
template <typename U>
allocator_inlined(const allocator_inlined<U, N, typename BaseAllocator::template rebind<U>::other>& other) noexcept {
FASTLED_UNUSED(other);
}
// Destructor
~allocator_inlined() noexcept {
clear();
}
// Allocate memory for n objects of type T
T* allocate(fl::size n) {
if (n == 0) {
return nullptr;
}
// For large allocations (n > 1), use base allocator directly
if (n > 1) {
T* ptr = m_base_allocator.allocate(n);
if (ptr) {
m_active_allocations += n;
}
return ptr;
}
// For single allocations, first try inlined memory
// Find first free inlined slot
fl::i32 free_slot = m_free_bits.find_first(false);
if (free_slot >= 0 && static_cast<fl::size>(free_slot) < N) {
// Mark the inlined slot as used
m_free_bits.set(static_cast<fl::u32>(free_slot), true);
// Update inlined usage tracking
if (static_cast<fl::size>(free_slot) + 1 > m_inlined_used) {
m_inlined_used = static_cast<fl::size>(free_slot) + 1;
}
m_active_allocations++;
return &get_inlined_ptr()[static_cast<fl::size>(free_slot)];
}
// No inlined slots available, use heap allocation
T* ptr = m_base_allocator.allocate(1);
if (ptr) {
m_active_allocations++;
}
return ptr;
}
// Deallocate memory for n objects of type T
void deallocate(T* p, fl::size n) {
if (!p || n == 0) {
return;
}
// Check if this is inlined memory
T* inlined_start = get_inlined_ptr();
T* inlined_end = inlined_start + N;
if (p >= inlined_start && p < inlined_end) {
// This is inlined memory, mark slots as free
fl::size slot_index = (p - inlined_start);
for (fl::size i = 0; i < n; ++i) {
if (slot_index + i < N) {
m_free_bits.set(slot_index + i, false); // Mark as free
}
}
m_active_allocations -= n;
return;
}
// Fallback to base allocator for heap allocations
m_base_allocator.deallocate(p, n);
m_active_allocations -= n;
}
// Construct an object at the specified address
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
if (p == nullptr) return;
new(static_cast<void*>(p)) U(fl::forward<Args>(args)...);
}
// Destroy an object at the specified address
template <typename U>
void destroy(U* p) {
if (p == nullptr) return;
p->~U();
}
// Clear all allocated memory
void clear() {
// Destroy inlined objects
for (fl::size i = 0; i < m_inlined_used; ++i) {
get_inlined_ptr()[i].~T();
}
m_inlined_used = 0;
m_free_bits.reset();
m_active_allocations = 0;
// Clean up the base allocator (for SlabAllocator, this clears slabs and free lists)
cleanup_base_allocator();
}
// Get total allocated size
fl::size total_size() const {
return m_active_allocations;
}
// Get inlined capacity
fl::size inlined_capacity() const {
return N;
}
// Check if using inlined storage
bool is_using_inlined() const {
return m_active_allocations == m_inlined_used;
}
private:
T* get_inlined_ptr() {
return reinterpret_cast<T*>(m_inlined_storage.data);
}
const T* get_inlined_ptr() const {
return reinterpret_cast<const T*>(m_inlined_storage.data);
}
// SFINAE helper to detect if base allocator has cleanup() method
template<typename U>
static auto has_cleanup_impl(int) -> decltype(fl::declval<U>().cleanup(), fl::true_type{});
template<typename U>
static fl::false_type has_cleanup_impl(...);
using has_cleanup = decltype(has_cleanup_impl<BaseAllocator>(0));
// Call cleanup on base allocator if it has the method
void cleanup_base_allocator() {
cleanup_base_allocator_impl(has_cleanup{});
}
void cleanup_base_allocator_impl(fl::true_type) {
m_base_allocator.cleanup();
}
void cleanup_base_allocator_impl(fl::false_type) {
// Base allocator doesn't have cleanup method, do nothing
}
// Equality comparison
bool operator==(const allocator_inlined& other) const noexcept {
FASTLED_UNUSED(other);
return true; // All instances are equivalent for now
}
bool operator!=(const allocator_inlined& other) const noexcept {
return !(*this == other);
}
};
// Inlined allocator that uses PSRam for heap allocation
template <typename T, fl::size N>
using allocator_inlined_psram = allocator_inlined<T, N, fl::allocator_psram<T>>;
// Inlined allocator that uses slab allocator for heap allocation
template <typename T, fl::size N, fl::size SLAB_SIZE = 8>
using allocator_inlined_slab_psram = allocator_inlined<T, N, fl::allocator_slab<T, SLAB_SIZE>>;
template <typename T, fl::size N>
using allocator_inlined_slab = allocator_inlined<T, N, fl::allocator_slab<T>>;
} // namespace fl

View File

@@ -0,0 +1,201 @@
// allow-include-after-namespace
#pragma once
#include <string.h>
#include "fl/inplacenew.h"
#include "fl/memfill.h"
#include "fl/type_traits.h"
#include "fl/bit_cast.h"
#include "fl/initializer_list.h"
#include "fl/has_include.h"
namespace fl {
/**
* @brief A fixed-size array implementation similar to std::array
*
* This class provides a thin wrapper around a C-style array with
* STL container-like interface.
*
* @tparam T The type of elements
* @tparam N The number of elements
*/
template <typename T, fl::size N> class array {
public:
// Standard container type definitions
using value_type = T;
using size_type = fl::size;
using difference_type = ptrdiff_t;
using reference = value_type &;
using const_reference = const value_type &;
using pointer = value_type *;
using const_pointer = const value_type *;
using iterator = pointer;
using const_iterator = const_pointer;
// Default constructor - elements are default-initialized
array() = default;
// Fill constructor
explicit array(const T &value) {
// std::fill_n(begin(), N, value);
fill_n(data_, N, value);
}
// Initializer list constructor
array(fl::initializer_list<T> list) {
fl::size i = 0;
for (auto it = list.begin(); it != list.end() && i < N; ++it, ++i) {
data_[i] = *it;
}
}
// Copy constructor
array(const array &) = default;
// Move constructor
array(array &&) = default;
// Copy assignment
array &operator=(const array &) = default;
// Move assignment
array &operator=(array &&) = default;
// Element access
T &at(fl::size pos) {
if (pos >= N) {
return error_value();
}
return data_[pos];
}
const T &at(fl::size pos) const {
if (pos >= N) {
return error_value();
}
return data_[pos];
}
T &operator[](fl::size pos) { return data_[pos]; }
const_reference operator[](fl::size pos) const { return data_[pos]; }
T &front() { return data_[0]; }
const T &front() const { return data_[0]; }
T &back() { return data_[N - 1]; }
const T &back() const { return data_[N - 1]; }
pointer data() noexcept { return data_; }
const_pointer data() const noexcept { return data_; }
// Iterators
iterator begin() noexcept { return data_; }
const_iterator begin() const noexcept { return data_; }
const_iterator cbegin() const noexcept { return data_; }
iterator end() noexcept { return data_ + N; }
const_iterator end() const noexcept { return data_ + N; }
// Capacity
bool empty() const noexcept { return N == 0; }
fl::size size() const noexcept { return N; }
fl::size max_size() const noexcept { return N; }
// Operations
void fill(const T &value) {
for (fl::size i = 0; i < N; ++i) {
data_[i] = value;
}
}
void swap(array &other) {
for (fl::size i = 0; i < N; ++i) {
fl::swap(data_[i], other.data_[i]);
}
}
private:
static T &error_value() {
static T empty_value;
return empty_value;
}
T data_[N];
};
// Non-member functions
template <typename T, fl::size N>
bool operator==(const array<T, N> &lhs, const array<T, N> &rhs) {
// return std::equal(lhs.begin(), lhs.end(), rhs.begin());
for (fl::size i = 0; i < N; ++i) {
if (lhs[i] != rhs[i]) {
return false;
}
}
}
template <typename T, fl::size N>
bool operator!=(const array<T, N> &lhs, const array<T, N> &rhs) {
return !(lhs == rhs);
}
template <typename T, fl::size N>
void swap(array<T, N> &lhs,
array<T, N> &rhs) noexcept(noexcept(lhs.swap(rhs))) {
lhs.swap(rhs);
}
} // namespace fl
// FASTLED_STACK_ARRAY
// An array of variable length that is allocated on the stack using
// either alloca or a variable length array (VLA) support built into the
// the compiler.
// Example:
// Instead of: int array[buff_size];
// You'd use: FASTLED_STACK_ARRAY(int, array, buff_size);
#ifndef FASTLED_VARIABLE_LENGTH_ARRAY_NEEDS_EMULATION
#if defined(__clang__) || defined(ARDUINO_GIGA_M7) || defined(ARDUINO_GIGA)
// Clang doesn't have variable length arrays. Therefore we need to emulate them
// using alloca. It's been found that Arduino Giga M7 also doesn't support
// variable length arrays for some reason so we force it to emulate them as well
// in this case.
#define FASTLED_VARIABLE_LENGTH_ARRAY_NEEDS_EMULATION 1
#else
// Else, assume the compiler is gcc, which has variable length arrays
#define FASTLED_VARIABLE_LENGTH_ARRAY_NEEDS_EMULATION 0
#endif
#endif // FASTLED_VARIABLE_LENGTH_ARRAY_NEEDS_EMULATION
#if !FASTLED_VARIABLE_LENGTH_ARRAY_NEEDS_EMULATION
#define FASTLED_STACK_ARRAY(TYPE, NAME, SIZE) \
TYPE NAME[SIZE]; \
fl::memfill(NAME, 0, sizeof(TYPE) * (SIZE))
#elif FL_HAS_INCLUDE(<alloca.h>)
#include <alloca.h>
#define FASTLED_STACK_ARRAY(TYPE, NAME, SIZE) \
TYPE *NAME = fl::bit_cast_ptr<TYPE>(alloca(sizeof(TYPE) * (SIZE))); \
fl::memfill(NAME, 0, sizeof(TYPE) * (SIZE))
#elif FL_HAS_INCLUDE(<cstdlib>)
#include <cstdlib> // ok include
#define FASTLED_STACK_ARRAY(TYPE, NAME, SIZE) \
TYPE *NAME = fl::bit_cast_ptr<TYPE>(alloca(sizeof(TYPE) * (SIZE))); \
fl::memfill(NAME, 0, sizeof(TYPE) * (SIZE))
#else
#error "Compiler does not allow variable type arrays."
#endif

View File

@@ -0,0 +1,8 @@
#pragma once
#include "platforms/assert_defs.h"
#ifndef FL_ASSERT
#define FL_ASSERT(x, MSG) FASTLED_ASSERT(x, MSG)
#define FL_ASSERT_IF FASTLED_ASSERT_IF
#endif

View File

@@ -0,0 +1,211 @@
#include "fl/async.h"
#include "fl/functional.h"
#include "fl/singleton.h"
#include "fl/algorithm.h"
#include "fl/task.h"
#include "fl/time.h"
#include "fl/warn.h"
// Platform-specific includes
#ifdef __EMSCRIPTEN__
extern "C" void emscripten_sleep(unsigned int ms);
#endif
namespace fl {
AsyncManager& AsyncManager::instance() {
return fl::Singleton<AsyncManager>::instance();
}
void AsyncManager::register_runner(async_runner* runner) {
if (runner && fl::find(mRunners.begin(), mRunners.end(), runner) == mRunners.end()) {
mRunners.push_back(runner);
}
}
void AsyncManager::unregister_runner(async_runner* runner) {
auto it = fl::find(mRunners.begin(), mRunners.end(), runner);
if (it != mRunners.end()) {
mRunners.erase(it);
}
}
void AsyncManager::update_all() {
// Update all registered runners
for (auto* runner : mRunners) {
if (runner) {
runner->update();
}
}
}
bool AsyncManager::has_active_tasks() const {
for (const auto* runner : mRunners) {
if (runner && runner->has_active_tasks()) {
return true;
}
}
return false;
}
size_t AsyncManager::total_active_tasks() const {
size_t total = 0;
for (const auto* runner : mRunners) {
if (runner) {
total += runner->active_task_count();
}
}
return total;
}
// Public API functions
void async_run() {
fl::Scheduler::instance().update();
AsyncManager::instance().update_all();
}
void async_yield() {
// Always pump all async tasks first
async_run();
// Platform-specific yielding behavior
#ifdef __EMSCRIPTEN__
// WASM: Use emscripten_sleep to yield control to browser event loop
emscripten_sleep(1); // Sleep for 1ms to yield to browser
#endif
for (int i = 0; i < 5; ++i) {
async_run(); // Give other async tasks a chance
}
}
size_t async_active_tasks() {
return AsyncManager::instance().total_active_tasks();
}
bool async_has_tasks() {
return AsyncManager::instance().has_active_tasks();
}
// Scheduler implementation
Scheduler& Scheduler::instance() {
return fl::Singleton<Scheduler>::instance();
}
int Scheduler::add_task(task t) {
if (t.get_impl()) {
t.get_impl()->mTaskId = mNextTaskId++;
int task_id = t.get_impl()->mTaskId;
mTasks.push_back(fl::move(t));
return task_id;
}
return 0; // Invalid task
}
void Scheduler::update() {
uint32_t current_time = fl::time();
// Use index-based iteration to avoid iterator invalidation issues
for (fl::size i = 0; i < mTasks.size();) {
task& t = mTasks[i];
auto impl = t.get_impl();
if (!impl || impl->is_canceled()) {
// erase() returns bool in HeapVector, not iterator
mTasks.erase(mTasks.begin() + i);
// Don't increment i since we just removed an element
} else {
// Check if task is ready to run (frame tasks will return false here)
bool should_run = impl->ready_to_run(current_time);
if (should_run) {
// Update last run time for recurring tasks
impl->set_last_run_time(current_time);
// Execute the task
if (impl->has_then()) {
impl->execute_then();
} else {
warn_no_then(impl->id(), impl->trace_label());
}
// Remove one-shot tasks, keep recurring ones
bool is_recurring = (impl->type() == TaskType::kEveryMs || impl->type() == TaskType::kAtFramerate);
if (is_recurring) {
++i; // Keep recurring tasks
} else {
// erase() returns bool in HeapVector, not iterator
mTasks.erase(mTasks.begin() + i);
// Don't increment i since we just removed an element
}
} else {
++i;
}
}
}
}
void Scheduler::update_before_frame_tasks() {
update_tasks_of_type(TaskType::kBeforeFrame);
}
void Scheduler::update_after_frame_tasks() {
update_tasks_of_type(TaskType::kAfterFrame);
}
void Scheduler::update_tasks_of_type(TaskType task_type) {
uint32_t current_time = fl::time();
// Use index-based iteration to avoid iterator invalidation issues
for (fl::size i = 0; i < mTasks.size();) {
task& t = mTasks[i];
auto impl = t.get_impl();
if (!impl || impl->is_canceled()) {
// erase() returns bool in HeapVector, not iterator
mTasks.erase(mTasks.begin() + i);
// Don't increment i since we just removed an element
} else if (impl->type() == task_type) {
// This is a frame task of the type we're looking for
bool should_run = impl->ready_to_run_frame_task(current_time);
if (should_run) {
// Update last run time for frame tasks (though they don't use it)
impl->set_last_run_time(current_time);
// Execute the task
if (impl->has_then()) {
impl->execute_then();
} else {
warn_no_then(impl->id(), impl->trace_label());
}
// Frame tasks are always one-shot, so remove them after execution
mTasks.erase(mTasks.begin() + i);
// Don't increment i since we just removed an element
} else {
++i;
}
} else {
++i; // Not the task type we're looking for
}
}
}
void Scheduler::warn_no_then(int task_id, const fl::string& trace_label) {
if (!trace_label.empty()) {
FL_WARN(fl::string("[fl::task] Warning: no then() callback set for Task#") << task_id << " launched at " << trace_label);
} else {
FL_WARN(fl::string("[fl::task] Warning: no then() callback set for Task#") << task_id);
}
}
void Scheduler::warn_no_catch(int task_id, const fl::string& trace_label, const Error& error) {
if (!trace_label.empty()) {
FL_WARN(fl::string("[fl::task] Warning: no catch_() callback set for Task#") << task_id << " launched at " << trace_label << ". Error: " << error.message);
} else {
FL_WARN(fl::string("[fl/task] Warning: no catch_() callback set for Task#") << task_id << ". Error: " << error.message);
}
}
} // namespace fl

View File

@@ -0,0 +1,260 @@
#pragma once
/// @file async.h
/// @brief Generic asynchronous task management for FastLED
///
/// This module provides a unified system for managing asynchronous operations
/// across FastLED, including HTTP requests, timers, and other background tasks.
///
/// The async system integrates with FastLED's engine events and can be pumped
/// during delay() calls on WASM platforms for optimal responsiveness.
///
/// @section Usage
/// @code
/// #include "fl/async.h"
///
/// // Create a custom async runner
/// class Myasync_runner : public fl::async_runner {
/// public:
/// void update() override {
/// // Process your async tasks here
/// process_timers();
/// handle_network_events();
/// }
///
/// bool has_active_tasks() const override {
/// return !mTimers.empty() || mNetworkActive;
/// }
///
/// size_t active_task_count() const override {
/// return mTimers.size() + (mNetworkActive ? 1 : 0);
/// }
/// };
///
/// void setup() {
/// Myasync_runner* runner = new Myasync_runner();
/// fl::AsyncManager::instance().register_runner(runner);
///
/// // Now your async tasks will be automatically updated during:
/// // - FastLED.show() calls (via engine events)
/// // - delay() calls on WASM platforms
/// // - Manual fl::async_run() calls
/// }
/// @endcode
#include "fl/namespace.h"
#include "fl/vector.h"
#include "fl/function.h"
#include "fl/ptr.h"
#include "fl/variant.h"
#include "fl/promise.h"
#include "fl/promise_result.h"
#include "fl/singleton.h"
#include "fl/thread_local.h"
#include "fl/task.h"
#include "fl/time.h"
namespace fl {
// Forward declarations
class AsyncManager;
/// @brief Generic asynchronous task runner interface
class async_runner {
public:
virtual ~async_runner() = default;
/// Update this async runner (called during async pumping)
virtual void update() = 0;
/// Check if this runner has active tasks
virtual bool has_active_tasks() const = 0;
/// Get number of active tasks (for debugging/monitoring)
virtual size_t active_task_count() const = 0;
};
/// @brief Async task manager (singleton)
class AsyncManager {
public:
static AsyncManager& instance();
/// Register an async runner
void register_runner(async_runner* runner);
/// Unregister an async runner
void unregister_runner(async_runner* runner);
/// Update all registered async runners
void update_all();
/// Check if there are any active async tasks
bool has_active_tasks() const;
/// Get total number of active tasks across all runners
size_t total_active_tasks() const;
private:
fl::vector<async_runner*> mRunners;
};
/// @brief Platform-specific async yield function
///
/// This function pumps all async tasks and yields control appropriately for the platform:
/// - WASM: calls async_run() then emscripten_sleep(1) to yield to browser
/// - Other platforms: calls async_run() multiple times with simple yielding
///
/// This centralizes platform-specific async behavior instead of having #ifdef in generic code.
void async_yield();
/// @brief Run all registered async tasks once
///
/// This function updates all registered async runners (fetch, timers, etc.)
/// and is automatically called during:
/// - FastLED engine events (onEndFrame)
/// - delay() calls on WASM platforms (every 1ms)
/// - Manual calls for custom async pumping
///
/// @note This replaces the old fetch_update() function with a generic approach
void async_run();
/// @brief Get the number of active async tasks across all systems
/// @return Total number of active async tasks
size_t async_active_tasks();
/// @brief Check if any async systems have active tasks
/// @return True if any async tasks are running
bool async_has_tasks();
/// @brief Synchronously wait for a promise to complete (ONLY safe in top-level contexts)
/// @tparam T The type of value the promise resolves to (automatically deduced)
/// @param promise The promise to wait for
/// @return A PromiseResult containing either the resolved value T or an Error
///
/// This function blocks until the promise is either resolved or rejected,
/// then returns a PromiseResult that can be checked with ok() for success/failure.
/// While waiting, it continuously calls async_yield() to pump async tasks and yield appropriately.
///
/// **SAFETY WARNING**: This function should ONLY be called from top-level contexts
/// like Arduino loop() function. Never call this from:
/// - Promise callbacks (.then, .catch_)
/// - Nested async operations
/// - Interrupt handlers
/// - Library initialization code
///
/// The "_top_level" suffix emphasizes this safety requirement.
///
/// **Type Deduction**: The template parameter T is automatically deduced from the
/// promise parameter, so you don't need to specify it explicitly.
///
/// @section Usage
/// @code
/// auto promise = fl::fetch_get("http://example.com");
/// auto result = fl::await_top_level(promise); // Type automatically deduced!
///
/// if (result.ok()) {
/// const Response& resp = result.value();
/// FL_WARN("Success: " << resp.text());
/// } else {
/// FL_WARN("Error: " << result.error().message);
/// }
///
/// // Or use operator bool
/// if (result) {
/// FL_WARN("Got response: " << result.value().status());
/// }
///
/// // You can still specify the type explicitly if needed:
/// auto explicit_result = fl::await_top_level<response>(promise);
/// @endcode
template<typename T>
fl::result<T> await_top_level(fl::promise<T> promise) {
// Handle invalid promises
if (!promise.valid()) {
return fl::result<T>(Error("Invalid promise"));
}
// If already completed, return immediately
if (promise.is_completed()) {
if (promise.is_resolved()) {
return fl::result<T>(promise.value());
} else {
return fl::result<T>(promise.error());
}
}
// Track recursion depth to prevent infinite loops
static fl::ThreadLocal<int> await_depth(0);
if (await_depth.access() > 10) {
return fl::result<T>(Error("await_top_level recursion limit exceeded - possible infinite loop"));
}
++await_depth.access();
// Wait for promise to complete while pumping async tasks
int pump_count = 0;
const int max_pump_iterations = 10000; // Safety limit
while (!promise.is_completed() && pump_count < max_pump_iterations) {
// Update the promise first (in case it's not managed by async system)
promise.update();
// Check if completed after update
if (promise.is_completed()) {
break;
}
// Platform-agnostic async pump and yield
async_yield();
++pump_count;
}
--await_depth.access();
// Check for timeout
if (pump_count >= max_pump_iterations) {
return fl::result<T>(Error("await_top_level timeout - promise did not complete"));
}
// Return the result
if (promise.is_resolved()) {
return fl::result<T>(promise.value());
} else {
return fl::result<T>(promise.error());
}
}
class Scheduler {
public:
static Scheduler& instance();
int add_task(task t);
void update();
// New methods for frame task handling
void update_before_frame_tasks();
void update_after_frame_tasks();
// For testing: clear all tasks
void clear_all_tasks() { mTasks.clear(); mNextTaskId = 1; }
private:
friend class fl::Singleton<Scheduler>;
Scheduler() : mTasks() {}
void warn_no_then(int task_id, const fl::string& trace_label);
void warn_no_catch(int task_id, const fl::string& trace_label, const Error& error);
// Helper method for running specific task types
void update_tasks_of_type(TaskType task_type);
fl::vector<task> mTasks;
int mNextTaskId = 1;
};
} // namespace fl

View File

@@ -0,0 +1,160 @@
#pragma once
#include "fl/thread.h"
#include "fl/int.h"
#include "fl/align.h"
#if FASTLED_MULTITHREADED
#include <atomic>
#endif
namespace fl {
#if FASTLED_MULTITHREADED
template <typename T>
using atomic = std::atomic<T>;
#else
template <typename T> class AtomicFake;
template <typename T>
using atomic = AtomicFake<T>;
#endif
using atomic_bool = atomic<bool>;
using atomic_int = atomic<int>;
using atomic_uint = atomic<unsigned int>;
using atomic_u32 = atomic<fl::u32>;
using atomic_i32 = atomic<fl::i32>;
///////////////////// IMPLEMENTATION //////////////////////////////////////
template <typename T> class AtomicFake {
public:
AtomicFake() : mValue{} {}
explicit AtomicFake(T value) : mValue(value) {}
// Non-copyable and non-movable
AtomicFake(const AtomicFake&) = delete;
AtomicFake& operator=(const AtomicFake&) = delete;
AtomicFake(AtomicFake&&) = delete;
AtomicFake& operator=(AtomicFake&&) = delete;
// Basic atomic operations - fake implementation (not actually atomic)
T load() const {
return mValue;
}
void store(T value) {
mValue = value;
}
T exchange(T value) {
T old = mValue;
mValue = value;
return old;
}
bool compare_exchange_weak(T& expected, T desired) {
if (mValue == expected) {
mValue = desired;
return true;
} else {
expected = mValue;
return false;
}
}
bool compare_exchange_strong(T& expected, T desired) {
return compare_exchange_weak(expected, desired);
}
// Assignment operator
T operator=(T value) {
store(value);
return value;
}
// Conversion operator
operator T() const {
return load();
}
// Arithmetic operators (for integral and floating point types)
T operator++() {
return ++mValue;
}
T operator++(int) {
return mValue++;
}
T operator--() {
return --mValue;
}
T operator--(int) {
return mValue--;
}
T operator+=(T value) {
mValue += value;
return mValue;
}
T operator-=(T value) {
mValue -= value;
return mValue;
}
T operator&=(T value) {
mValue &= value;
return mValue;
}
T operator|=(T value) {
mValue |= value;
return mValue;
}
T operator^=(T value) {
mValue ^= value;
return mValue;
}
// Fetch operations
T fetch_add(T value) {
T old = mValue;
mValue += value;
return old;
}
T fetch_sub(T value) {
T old = mValue;
mValue -= value;
return old;
}
T fetch_and(T value) {
T old = mValue;
mValue &= value;
return old;
}
T fetch_or(T value) {
T old = mValue;
mValue |= value;
return old;
}
T fetch_xor(T value) {
T old = mValue;
mValue ^= value;
return old;
}
private:
FL_ALIGN_AS(T) T mValue;
};
} // namespace fl

View File

@@ -0,0 +1,194 @@
#include "audio.h"
#include "fl/thread_local.h"
#include "fl/int.h"
#include "fl/mutex.h"
namespace fl {
namespace {
FFT &get_flex_fft() {
static ThreadLocal<FFT> gFlexFFT;
return gFlexFFT.access();
}
// Object pool implementation
struct AudioSamplePool {
static AudioSamplePool& instance() {
static AudioSamplePool s_pool;
return s_pool;
}
void put(AudioSampleImplPtr&& impl) {
if (impl.unique()) {
// There is no more shared_ptr to this object, so we can recycle it.
fl::lock_guard<fl::mutex> lock(mutex);
if (impl && pool.size() < MAX_POOL_SIZE) {
// Reset the impl for reuse (clear internal state)
impl->reset();
pool.push_back(impl);
return;
}
}
// Pool is full, discard the impl
impl.reset();
}
AudioSampleImplPtr getOrCreate() {
{
fl::lock_guard<fl::mutex> lock(mutex);
if (!pool.empty()) {
AudioSampleImplPtr impl = pool.back();
pool.pop_back();
return impl;
}
}
return fl::make_shared<AudioSampleImpl>();
}
fl::vector<AudioSampleImplPtr> pool;
static constexpr fl::size MAX_POOL_SIZE = 8;
fl::mutex mutex;
};
} // namespace
AudioSample::~AudioSample() {
if (mImpl) {
AudioSamplePool::instance().put(fl::move(mImpl));
}
}
const AudioSample::VectorPCM &AudioSample::pcm() const {
if (isValid()) {
return mImpl->pcm();
}
static VectorPCM empty;
return empty;
}
AudioSample &AudioSample::operator=(const AudioSample &other) {
mImpl = other.mImpl;
return *this;
}
fl::size AudioSample::size() const {
if (isValid()) {
return mImpl->pcm().size();
}
return 0;
}
const fl::i16 &AudioSample::at(fl::size i) const {
if (i < size()) {
return pcm()[i];
}
return empty()[0];
}
const fl::i16 &AudioSample::operator[](fl::size i) const { return at(i); }
bool AudioSample::operator==(const AudioSample &other) const {
if (mImpl == other.mImpl) {
return true;
}
if (mImpl == nullptr || other.mImpl == nullptr) {
return false;
}
if (mImpl->pcm().size() != other.mImpl->pcm().size()) {
return false;
}
for (fl::size i = 0; i < mImpl->pcm().size(); ++i) {
if (mImpl->pcm()[i] != other.mImpl->pcm()[i]) {
return false;
}
}
return true;
}
bool AudioSample::operator!=(const AudioSample &other) const {
return !(*this == other);
}
const AudioSample::VectorPCM &AudioSample::empty() {
static fl::i16 empty_data[1] = {0};
static VectorPCM empty(empty_data);
return empty;
}
float AudioSample::zcf() const { return mImpl->zcf(); }
fl::u32 AudioSample::timestamp() const {
if (isValid()) {
return mImpl->timestamp();
}
return 0;
}
float AudioSample::rms() const {
if (!isValid()) {
return 0.0f;
}
fl::u64 sum_sq = 0;
const int N = size();
for (int i = 0; i < N; ++i) {
fl::i32 x32 = fl::i32(pcm()[i]);
sum_sq += x32 * x32;
}
float rms = sqrtf(float(sum_sq) / N);
return rms;
}
SoundLevelMeter::SoundLevelMeter(double spl_floor, double smoothing_alpha)
: spl_floor_(spl_floor), smoothing_alpha_(smoothing_alpha),
dbfs_floor_global_(INFINITY_DOUBLE), offset_(0.0), current_dbfs_(0.0),
current_spl_(spl_floor) {}
void SoundLevelMeter::processBlock(const fl::i16 *samples, fl::size count) {
// 1) compute block power → dBFS
double sum_sq = 0.0;
for (fl::size i = 0; i < count; ++i) {
double s = samples[i] / 32768.0; // normalize to ±1
sum_sq += s * s;
}
double p = sum_sq / count; // mean power
double dbfs = 10.0 * log10(p + 1e-12);
current_dbfs_ = dbfs;
// 2) update global floor (with optional smoothing)
if (dbfs < dbfs_floor_global_) {
if (smoothing_alpha_ <= 0.0) {
dbfs_floor_global_ = dbfs;
} else {
dbfs_floor_global_ = smoothing_alpha_ * dbfs +
(1.0 - smoothing_alpha_) * dbfs_floor_global_;
}
offset_ = spl_floor_ - dbfs_floor_global_;
}
// 3) estimate SPL
current_spl_ = dbfs + offset_;
}
void AudioSample::fft(FFTBins *out) const {
fl::span<const fl::i16> sample = pcm();
FFT_Args args;
args.samples = sample.size();
args.bands = out->size();
args.fmin = FFT_Args::DefaultMinFrequency();
args.fmax = FFT_Args::DefaultMaxFrequency();
args.sample_rate =
FFT_Args::DefaultSampleRate(); // TODO: get sample rate from AudioSample
get_flex_fft().run(sample, out, args);
}
AudioSample::AudioSample(fl::span<const fl::i16> span, fl::u32 timestamp) {
mImpl = AudioSamplePool::instance().getOrCreate();
auto begin = span.data();
auto end = begin + span.size();
mImpl->assign(begin, end, timestamp);
}
} // namespace fl

View File

@@ -0,0 +1,169 @@
#pragma once
#include "fl/fft.h"
#include "fl/math.h"
#include "fl/memory.h"
#include "fl/span.h"
#include "fl/vector.h"
#include "fl/int.h"
#include <math.h>
#include "fl/stdint.h"
#include "fl/span.h"
namespace fl {
class AudioSampleImpl;
FASTLED_SMART_PTR(AudioSampleImpl);
// AudioSample is a wrapper around AudioSampleImpl, hiding the reference
// counting so that the api object can be simple and have standard object
// semantics.
class AudioSample {
public:
using VectorPCM = fl::vector<fl::i16>;
using const_iterator = VectorPCM::const_iterator;
AudioSample() {}
AudioSample(const AudioSample &other) : mImpl(other.mImpl) {}
AudioSample(AudioSampleImplPtr impl) : mImpl(impl) {}
~AudioSample();
// Constructor that takes raw audio data and handles pooling internally
AudioSample(fl::span<const fl::i16> span, fl::u32 timestamp = 0);
AudioSample &operator=(const AudioSample &other);
bool isValid() const { return mImpl != nullptr; }
fl::size size() const;
// Raw pcm levels.
const VectorPCM &pcm() const;
// Zero crossing factor between 0.0f -> 1.0f, detects "hiss"
// and sounds like cloths rubbing. Useful for sound analysis.
float zcf() const;
float rms() const;
fl::u32 timestamp() const; // Timestamp when sample became valid (millis)
void fft(FFTBins *out) const;
const_iterator begin() const { return pcm().begin(); }
const_iterator end() const { return pcm().end(); }
const fl::i16 &at(fl::size i) const;
const fl::i16 &operator[](fl::size i) const;
operator bool() const { return isValid(); }
bool operator==(const AudioSample &other) const;
bool operator!=(const AudioSample &other) const;
private:
static const VectorPCM &empty();
AudioSampleImplPtr mImpl;
};
// Sound level meter is a persistant measuring class that will auto-tune the
// microphone to real world SPL levels. It will adapt to the noise floor of the
// environment. Note that the microphone only ever outputs DBFS (dB Full Scale)
// values, which are collected over a stream of samples. The sound level meter
// will convert this to SPL (Sound Pressure Level) values, which are the real
// world values.
class SoundLevelMeter {
public:
/// @param spl_floor The SPL (dB SPL) that corresponds to your true
/// noise-floor.
/// @param smoothing_alpha [0…1] how quickly to adapt floor; 0=instant min.
SoundLevelMeter(double spl_floor = 33.0, double smoothing_alpha = 0.0);
/// Process a block of int16 PCM samples.
void processBlock(const fl::i16 *samples, fl::size count);
void processBlock(fl::span<const fl::i16> samples) {
processBlock(samples.data(), samples.size());
}
/// @returns most recent blocks level in dBFS (≤0)
double getDBFS() const { return current_dbfs_; }
/// @returns calibrated estimate in dB SPL
double getSPL() const { return current_spl_; }
/// change your known noise-floor SPL at runtime
void setFloorSPL(double spl_floor) {
spl_floor_ = spl_floor;
offset_ = spl_floor_ - dbfs_floor_global_;
}
/// reset so the next quiet block will re-initialize your floor
void resetFloor() {
dbfs_floor_global_ = INFINITY_DOUBLE; // infinity<double>
offset_ = 0.0;
}
private:
double spl_floor_; // e.g. 33.0 dB SPL
double smoothing_alpha_; // 0 = pure min, >0 = slow adapt
double dbfs_floor_global_; // lowest dBFS seen so far
double offset_; // spl_floor_ dbfs_floor_global_
double current_dbfs_; // last blocks dBFS
double current_spl_; // last blocks estimated SPL
};
// Implementation details.
class AudioSampleImpl {
public:
using VectorPCM = fl::vector<fl::i16>;
~AudioSampleImpl() {}
// template <typename It> void assign(It begin, It end) {
// assign(begin, end, 0); // Default timestamp to 0
// }
template <typename It> void assign(It begin, It end, fl::u32 timestamp) {
mSignedPcm.assign(begin, end);
mTimestamp = timestamp;
// calculate zero crossings
initZeroCrossings();
}
const VectorPCM &pcm() const { return mSignedPcm; }
fl::u32 timestamp() const { return mTimestamp; }
// For object pool - reset internal state for reuse
void reset() {
mSignedPcm.clear();
mZeroCrossings = 0;
mTimestamp = 0;
}
// "Zero crossing factor". High values > .4 indicate hissing
// sounds. For example a microphone rubbing against a clothing.
// These types of signals indicate the audio should be ignored.
// Low zero crossing factors (with loud sound) indicate that there
// is organized sound like that coming from music. This is so cheap
// to calculate it's done automatically. It should be one of the first
// signals to reject or accept a sound signal.
//
// Returns: a value -> [0.0f, 1.0f)
float zcf() const {
const fl::size n = pcm().size();
if (n < 2) {
return 0.f;
}
return float(mZeroCrossings) / static_cast<float>(n - 1);
}
private:
void initZeroCrossings() {
mZeroCrossings = 0;
if (mSignedPcm.size() > 1) {
for (fl::size i = 1; i < mSignedPcm.size(); ++i) {
const bool crossed =
(mSignedPcm[i - 1] < 0 && mSignedPcm[i] >= 0) ||
(mSignedPcm[i - 1] >= 0 && mSignedPcm[i] < 0);
if (crossed) {
++mZeroCrossings;
}
}
}
}
VectorPCM mSignedPcm;
fl::i16 mZeroCrossings = 0;
fl::u32 mTimestamp = 0;
};
} // namespace fl

View File

@@ -0,0 +1,74 @@
#include "fl/sketch_macros.h"
#include "fl/shared_ptr.h"
#include "fl/memory.h"
#include "fl/string.h"
#include "fl/compiler_control.h"
#include "fl/has_include.h"
#include "platforms/audio_input_null.hpp"
// Auto-determine Arduino usage if not explicitly set. Force this to 1
// if you want to test Arduino path for audio input on a platform with
// native audio support.
#ifndef FASTLED_USES_ARDUINO_AUDIO_INPUT
#if defined(ESP32) && !defined(ESP8266)
#define FASTLED_USES_ARDUINO_AUDIO_INPUT 0
#elif FL_HAS_INCLUDE(<Arduino.h>)
#define FASTLED_USES_ARDUINO_AUDIO_INPUT 1
#else
#define FASTLED_USES_ARDUINO_AUDIO_INPUT 0
#endif
#endif
#if !FASTLED_USES_ARDUINO_AUDIO_INPUT
#if defined(ESP32) && !defined(ESP8266)
#define FASTLED_USES_ESP32_AUDIO_INPUT 1
#else
#define FASTLED_USES_ESP32_AUDIO_INPUT 0
#endif
#else
#define FASTLED_USES_ESP32_AUDIO_INPUT 0
#endif
// Include ESP32 audio input implementation if on ESP32
#if FASTLED_USES_ARDUINO_AUDIO_INPUT
#include "platforms/arduino/audio_input.hpp"
#elif FASTLED_USES_ESP32_AUDIO_INPUT
#include "platforms/esp/32/audio/audio_impl.hpp"
#endif
namespace fl {
#if FASTLED_USES_ARDUINO_AUDIO_INPUT
// Use Arduino audio implementation
fl::shared_ptr<IAudioInput> platform_create_audio_input(const AudioConfig &config, fl::string *error_message) {
return arduino_create_audio_input(config, error_message);
}
#elif FASTLED_USES_ESP32_AUDIO_INPUT
// ESP32 native implementation
fl::shared_ptr<IAudioInput> platform_create_audio_input(const AudioConfig &config, fl::string *error_message) {
return esp32_create_audio_input(config, error_message);
}
#else
// Weak default implementation - no audio support
FL_LINK_WEAK
fl::shared_ptr<IAudioInput> platform_create_audio_input(const AudioConfig &config, fl::string *error_message) {
if (error_message) {
*error_message = "AudioInput not supported on this platform.";
}
return fl::make_shared<fl::Null_Audio>();
}
#endif
// Static method delegates to free function
fl::shared_ptr<IAudioInput>
IAudioInput::create(const AudioConfig &config, fl::string *error_message) {
return platform_create_audio_input(config, error_message);
}
} // namespace fl

View File

@@ -0,0 +1,142 @@
#pragma once
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
#include "fl/vector.h"
#include "fl/variant.h"
#include "fl/shared_ptr.h"
#include "fl/audio.h"
#include "fl/compiler_control.h"
#include "platforms/audio.h"
#ifndef FASTLED_HAS_AUDIO_INPUT
#error "platforms/audio.h must define FASTLED_HAS_AUDIO_INPUT"
#endif
#define I2S_AUDIO_BUFFER_LEN 512
#define AUDIO_DEFAULT_SAMPLE_RATE 44100ul
#define AUDIO_DEFAULT_BIT_RESOLUTION 16
#define AUDIO_DMA_BUFFER_COUNT 8
namespace fl {
// Note: Right now these are esp specific, but they are designed to migrate to a common api.
enum AudioChannel {
Left = 0,
Right = 1,
Both = 2, // Two microphones can be used to capture both channels with one AudioSource.
};
enum I2SCommFormat {
Philips = 0X01, // I2S communication I2S Philips standard, data launch at second BCK
MSB = 0X02, // I2S communication MSB alignment standard, data launch at first BCK
PCMShort = 0x04, // PCM Short standard, also known as DSP mode. The period of synchronization signal (WS) is 1 bck cycle.
PCMLong = 0x0C, // PCM Long standard. The period of synchronization signal (WS) is channel_bit*bck cycles.
Max = 0x0F, // standard max
};
struct AudioConfigI2S {
int mPinWs;
int mPinSd;
int mPinClk;
int mI2sNum;
AudioChannel mAudioChannel;
u16 mSampleRate;
u8 mBitResolution;
I2SCommFormat mCommFormat;
bool mInvert;
AudioConfigI2S(
int pin_ws,
int pin_sd,
int pin_clk,
int i2s_num,
AudioChannel mic_channel,
u16 sample_rate,
u8 bit_resolution,
I2SCommFormat comm_format = Philips,
bool invert = false
)
: mPinWs(pin_ws), mPinSd(pin_sd), mPinClk(pin_clk),
mI2sNum(i2s_num), mAudioChannel(mic_channel),
mSampleRate(sample_rate), mBitResolution(bit_resolution), mCommFormat(comm_format), mInvert(invert) {}
};
struct AudioConfigPdm {
int mPinDin;
int mPinClk;
int mI2sNum;
u16 mSampleRate;
bool mInvert = false;
AudioConfigPdm(int pin_din, int pin_clk, int i2s_num, u16 sample_rate = AUDIO_DEFAULT_SAMPLE_RATE, bool invert = false)
: mPinDin(pin_din), mPinClk(pin_clk), mI2sNum(i2s_num), mSampleRate(sample_rate), mInvert(invert) {}
};
class AudioConfig : public fl::Variant<AudioConfigI2S, AudioConfigPdm> {
public:
// The most common microphone on Amazon as of 2025-September.
static AudioConfig CreateInmp441(int pin_ws, int pin_sd, int pin_clk, AudioChannel channel, u16 sample_rate = 44100ul, int i2s_num = 0) {
AudioConfigI2S config(pin_ws, pin_sd, pin_clk, i2s_num, channel, sample_rate, 16);
return AudioConfig(config);
}
AudioConfig(const AudioConfigI2S& config) : fl::Variant<AudioConfigI2S, AudioConfigPdm>(config) {}
AudioConfig(const AudioConfigPdm& config) : fl::Variant<AudioConfigI2S, AudioConfigPdm>(config) {}
};
class IAudioInput {
public:
// This is the single factory function for creating the audio source. If the creation was successful, then
// the return value will be non-null. If the creation was not successful, then the return value will be null
// and the error_message will be set to a non-empty string.
// Keep in mind that the AudioConfig is a variant type. Many esp types do not support all the types in the variant.
// For example, the AudioConfigPdm is not supported on the ESP32-C3 and in this case it will return a null pointer
// and the error_message will be set to a non-empty string.
// Implimentation notes:
// It's very important that the implimentation uses a esp task / interrupt to fill in the buffer. The reason is that
// there will be looooong delays during FastLED show() on some esp platforms, for example idf 4.4. If we do
// poll only, then audio buffers can be dropped. However if using a task then the audio buffers will be
// set internally via an interrupt / queue and then they can just be popped off the queue.
static fl::shared_ptr<IAudioInput> create(const AudioConfig& config, fl::string* error_message = nullptr);
virtual ~IAudioInput() = default;
// Starts the audio source.
virtual void start() = 0;
// Stops the audio source, call this before light sleep.
virtual void stop() = 0;
virtual bool error(fl::string* msg = nullptr) = 0; // if an error occured then query it here.
// Read audio data and return as AudioSample with calculated timestamp.
// Returns invalid AudioSample on error or when no data is available.
virtual AudioSample read() = 0;
// Read all available audio data and return as AudioSample. All AudioSamples
// returned by this will be valid.
size_t readAll(fl::vector_inlined<AudioSample, 16> *out) {
size_t count = 0;
while (true) {
AudioSample sample = read();
if (sample.isValid()) {
out->push_back(sample);
count++;
} else {
break;
}
}
return count;
}
};
// Free function for audio input creation - can be overridden by platform-specific implementations
fl::shared_ptr<IAudioInput> platform_create_audio_input(const AudioConfig& config, fl::string* error_message = nullptr) FL_LINK_WEAK;
} // namespace fl

View File

@@ -0,0 +1,759 @@
#include "fl/audio_reactive.h"
#include "fl/math.h"
#include "fl/span.h"
#include "fl/int.h"
#include "fl/memory.h"
#include <math.h>
namespace fl {
AudioReactive::AudioReactive()
: mConfig{}, mFFTBins(16) // Initialize with 16 frequency bins
{
// Initialize enhanced beat detection components
mSpectralFluxDetector = fl::make_unique<SpectralFluxDetector>();
mPerceptualWeighting = fl::make_unique<PerceptualWeighting>();
// Initialize previous magnitudes array to zero
for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
mPreviousMagnitudes[i] = 0.0f;
}
}
AudioReactive::~AudioReactive() = default;
void AudioReactive::begin(const AudioReactiveConfig& config) {
setConfig(config);
// Reset state
mCurrentData = AudioData{};
mSmoothedData = AudioData{};
mLastBeatTime = 0;
mPreviousVolume = 0.0f;
mAGCMultiplier = 1.0f;
mMaxSample = 0.0f;
mAverageLevel = 0.0f;
// Reset enhanced beat detection components
if (mSpectralFluxDetector) {
mSpectralFluxDetector->reset();
mSpectralFluxDetector->setThreshold(config.spectralFluxThreshold);
}
// Reset previous magnitudes
for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
mPreviousMagnitudes[i] = 0.0f;
}
}
void AudioReactive::setConfig(const AudioReactiveConfig& config) {
mConfig = config;
}
void AudioReactive::processSample(const AudioSample& sample) {
if (!sample.isValid()) {
return; // Invalid sample, ignore
}
// Extract timestamp from the AudioSample
fl::u32 currentTimeMs = sample.timestamp();
// Process the AudioSample immediately - timing is gated by sample availability
processFFT(sample);
updateVolumeAndPeak(sample);
// Enhanced processing pipeline
calculateBandEnergies();
updateSpectralFlux();
// Enhanced beat detection (includes original)
detectBeat(currentTimeMs);
detectEnhancedBeats(currentTimeMs);
// Apply perceptual weighting if enabled
applyPerceptualWeighting();
applyGain();
applyScaling();
smoothResults();
mCurrentData.timestamp = currentTimeMs;
}
void AudioReactive::update(fl::u32 currentTimeMs) {
// This method handles updates without new sample data
// Just apply smoothing and update timestamp
smoothResults();
mCurrentData.timestamp = currentTimeMs;
}
void AudioReactive::processFFT(const AudioSample& sample) {
// Get PCM data from AudioSample
const auto& pcmData = sample.pcm();
if (pcmData.empty()) return;
// Use AudioSample's built-in FFT capability
sample.fft(&mFFTBins);
// Map FFT bins to frequency channels using WLED-compatible mapping
mapFFTBinsToFrequencyChannels();
}
void AudioReactive::mapFFTBinsToFrequencyChannels() {
// Copy FFT results to frequency bins array
for (int i = 0; i < 16; ++i) {
if (i < static_cast<int>(mFFTBins.bins_raw.size())) {
mCurrentData.frequencyBins[i] = mFFTBins.bins_raw[i];
} else {
mCurrentData.frequencyBins[i] = 0.0f;
}
}
// Apply pink noise compensation (from WLED)
for (int i = 0; i < 16; ++i) {
mCurrentData.frequencyBins[i] *= PINK_NOISE_COMPENSATION[i];
}
// Find dominant frequency
float maxMagnitude = 0.0f;
int maxBin = 0;
for (int i = 0; i < 16; ++i) {
if (mCurrentData.frequencyBins[i] > maxMagnitude) {
maxMagnitude = mCurrentData.frequencyBins[i];
maxBin = i;
}
}
// Convert bin index to approximate frequency
// Rough approximation based on WLED frequency mapping
const float binCenterFrequencies[16] = {
64.5f, // Bin 0: 43-86 Hz
107.5f, // Bin 1: 86-129 Hz
172.5f, // Bin 2: 129-216 Hz
258.5f, // Bin 3: 216-301 Hz
365.5f, // Bin 4: 301-430 Hz
495.0f, // Bin 5: 430-560 Hz
689.0f, // Bin 6: 560-818 Hz
969.0f, // Bin 7: 818-1120 Hz
1270.5f, // Bin 8: 1120-1421 Hz
1658.0f, // Bin 9: 1421-1895 Hz
2153.5f, // Bin 10: 1895-2412 Hz
2713.5f, // Bin 11: 2412-3015 Hz
3359.5f, // Bin 12: 3015-3704 Hz
4091.5f, // Bin 13: 3704-4479 Hz
5792.5f, // Bin 14: 4479-7106 Hz
8182.5f // Bin 15: 7106-9259 Hz
};
mCurrentData.dominantFrequency = binCenterFrequencies[maxBin];
mCurrentData.magnitude = maxMagnitude;
}
void AudioReactive::updateVolumeAndPeak(const AudioSample& sample) {
// Get PCM data from AudioSample
const auto& pcmData = sample.pcm();
if (pcmData.empty()) {
mCurrentData.volume = 0.0f;
mCurrentData.volumeRaw = 0.0f;
mCurrentData.peak = 0.0f;
return;
}
// Use AudioSample's built-in RMS calculation
float rms = sample.rms();
// Calculate peak from PCM data
float maxSample = 0.0f;
for (fl::i16 pcmSample : pcmData) {
float absSample = (pcmSample < 0) ? -pcmSample : pcmSample;
maxSample = (maxSample > absSample) ? maxSample : absSample;
}
// Scale to 0-255 range (approximately)
mCurrentData.volumeRaw = rms / 128.0f; // Rough scaling
mCurrentData.volume = mCurrentData.volumeRaw;
// Peak detection
mCurrentData.peak = maxSample / 32768.0f * 255.0f;
// Update AGC tracking
if (mConfig.agcEnabled) {
// AGC with attack/decay behavior
float agcAttackRate = mConfig.attack / 255.0f * 0.2f + 0.01f; // 0.01 to 0.21
float agcDecayRate = mConfig.decay / 255.0f * 0.05f + 0.001f; // 0.001 to 0.051
// Track maximum level with attack/decay
if (maxSample > mMaxSample) {
// Rising - use attack rate (faster response)
mMaxSample = mMaxSample * (1.0f - agcAttackRate) + maxSample * agcAttackRate;
} else {
// Falling - use decay rate (slower response)
mMaxSample = mMaxSample * (1.0f - agcDecayRate) + maxSample * agcDecayRate;
}
// Update AGC multiplier with proper bounds
if (mMaxSample > 1000.0f) {
float targetLevel = 16384.0f; // Half of full scale
float newMultiplier = targetLevel / mMaxSample;
// Smooth AGC multiplier changes using attack/decay
if (newMultiplier > mAGCMultiplier) {
// Increasing gain - use attack rate
mAGCMultiplier = mAGCMultiplier * (1.0f - agcAttackRate) + newMultiplier * agcAttackRate;
} else {
// Decreasing gain - use decay rate
mAGCMultiplier = mAGCMultiplier * (1.0f - agcDecayRate) + newMultiplier * agcDecayRate;
}
// Clamp multiplier to reasonable bounds
mAGCMultiplier = (mAGCMultiplier < 0.1f) ? 0.1f : ((mAGCMultiplier > 10.0f) ? 10.0f : mAGCMultiplier);
}
}
}
void AudioReactive::detectBeat(fl::u32 currentTimeMs) {
// Need minimum time since last beat
if (currentTimeMs - mLastBeatTime < BEAT_COOLDOWN) {
mCurrentData.beatDetected = false;
return;
}
// Simple beat detection based on volume increase
float currentVolume = mCurrentData.volume;
// Beat detected if volume significantly increased
if (currentVolume > mPreviousVolume + mVolumeThreshold &&
currentVolume > 5.0f) { // Minimum volume threshold
mCurrentData.beatDetected = true;
mLastBeatTime = currentTimeMs;
} else {
mCurrentData.beatDetected = false;
}
// Update previous volume for next comparison using attack/decay
float beatAttackRate = mConfig.attack / 255.0f * 0.5f + 0.1f; // 0.1 to 0.6
float beatDecayRate = mConfig.decay / 255.0f * 0.3f + 0.05f; // 0.05 to 0.35
if (currentVolume > mPreviousVolume) {
// Rising volume - use attack rate (faster tracking)
mPreviousVolume = mPreviousVolume * (1.0f - beatAttackRate) + currentVolume * beatAttackRate;
} else {
// Falling volume - use decay rate (slower tracking)
mPreviousVolume = mPreviousVolume * (1.0f - beatDecayRate) + currentVolume * beatDecayRate;
}
}
void AudioReactive::applyGain() {
// Apply gain setting (0-255 maps to 0.0-2.0 multiplier)
float gainMultiplier = static_cast<float>(mConfig.gain) / 128.0f;
mCurrentData.volume *= gainMultiplier;
mCurrentData.volumeRaw *= gainMultiplier;
mCurrentData.peak *= gainMultiplier;
for (int i = 0; i < 16; ++i) {
mCurrentData.frequencyBins[i] *= gainMultiplier;
}
// Apply AGC if enabled
if (mConfig.agcEnabled) {
mCurrentData.volume *= mAGCMultiplier;
mCurrentData.volumeRaw *= mAGCMultiplier;
mCurrentData.peak *= mAGCMultiplier;
for (int i = 0; i < 16; ++i) {
mCurrentData.frequencyBins[i] *= mAGCMultiplier;
}
}
}
void AudioReactive::applyScaling() {
// Apply scaling mode to frequency bins
for (int i = 0; i < 16; ++i) {
float value = mCurrentData.frequencyBins[i];
switch (mConfig.scalingMode) {
case 1: // Logarithmic scaling
if (value > 1.0f) {
value = logf(value) * 20.0f; // Scale factor
} else {
value = 0.0f;
}
break;
case 2: // Linear scaling (no change)
// value remains as-is
break;
case 3: // Square root scaling
if (value > 0.0f) {
value = sqrtf(value) * 8.0f; // Scale factor
} else {
value = 0.0f;
}
break;
case 0: // No scaling
default:
// value remains as-is
break;
}
mCurrentData.frequencyBins[i] = value;
}
}
void AudioReactive::smoothResults() {
// Attack/decay smoothing - different rates for rising vs falling values
// Convert attack/decay times to smoothing factors
// Shorter times = less smoothing (faster response)
float attackFactor = 1.0f - (mConfig.attack / 255.0f * 0.9f); // Range: 0.1 to 1.0
float decayFactor = 1.0f - (mConfig.decay / 255.0f * 0.95f); // Range: 0.05 to 1.0
// Apply attack/decay smoothing to volume
if (mCurrentData.volume > mSmoothedData.volume) {
// Rising - use attack time (faster response)
mSmoothedData.volume = mSmoothedData.volume * (1.0f - attackFactor) +
mCurrentData.volume * attackFactor;
} else {
// Falling - use decay time (slower response)
mSmoothedData.volume = mSmoothedData.volume * (1.0f - decayFactor) +
mCurrentData.volume * decayFactor;
}
// Apply attack/decay smoothing to volumeRaw
if (mCurrentData.volumeRaw > mSmoothedData.volumeRaw) {
mSmoothedData.volumeRaw = mSmoothedData.volumeRaw * (1.0f - attackFactor) +
mCurrentData.volumeRaw * attackFactor;
} else {
mSmoothedData.volumeRaw = mSmoothedData.volumeRaw * (1.0f - decayFactor) +
mCurrentData.volumeRaw * decayFactor;
}
// Apply attack/decay smoothing to peak
if (mCurrentData.peak > mSmoothedData.peak) {
mSmoothedData.peak = mSmoothedData.peak * (1.0f - attackFactor) +
mCurrentData.peak * attackFactor;
} else {
mSmoothedData.peak = mSmoothedData.peak * (1.0f - decayFactor) +
mCurrentData.peak * decayFactor;
}
// Apply attack/decay smoothing to frequency bins
for (int i = 0; i < 16; ++i) {
if (mCurrentData.frequencyBins[i] > mSmoothedData.frequencyBins[i]) {
// Rising - use attack time
mSmoothedData.frequencyBins[i] = mSmoothedData.frequencyBins[i] * (1.0f - attackFactor) +
mCurrentData.frequencyBins[i] * attackFactor;
} else {
// Falling - use decay time
mSmoothedData.frequencyBins[i] = mSmoothedData.frequencyBins[i] * (1.0f - decayFactor) +
mCurrentData.frequencyBins[i] * decayFactor;
}
}
// Copy non-smoothed values
mSmoothedData.beatDetected = mCurrentData.beatDetected;
mSmoothedData.dominantFrequency = mCurrentData.dominantFrequency;
mSmoothedData.magnitude = mCurrentData.magnitude;
mSmoothedData.timestamp = mCurrentData.timestamp;
}
const AudioData& AudioReactive::getData() const {
return mCurrentData;
}
const AudioData& AudioReactive::getSmoothedData() const {
return mSmoothedData;
}
float AudioReactive::getVolume() const {
return mCurrentData.volume;
}
float AudioReactive::getBass() const {
// Average of bins 0-1 (sub-bass and bass)
return (mCurrentData.frequencyBins[0] + mCurrentData.frequencyBins[1]) / 2.0f;
}
float AudioReactive::getMid() const {
// Average of bins 6-7 (midrange around 1kHz)
return (mCurrentData.frequencyBins[6] + mCurrentData.frequencyBins[7]) / 2.0f;
}
float AudioReactive::getTreble() const {
// Average of bins 14-15 (high frequencies)
return (mCurrentData.frequencyBins[14] + mCurrentData.frequencyBins[15]) / 2.0f;
}
bool AudioReactive::isBeat() const {
return mCurrentData.beatDetected;
}
bool AudioReactive::isBassBeat() const {
return mCurrentData.bassBeatDetected;
}
bool AudioReactive::isMidBeat() const {
return mCurrentData.midBeatDetected;
}
bool AudioReactive::isTrebleBeat() const {
return mCurrentData.trebleBeatDetected;
}
float AudioReactive::getSpectralFlux() const {
return mCurrentData.spectralFlux;
}
float AudioReactive::getBassEnergy() const {
return mCurrentData.bassEnergy;
}
float AudioReactive::getMidEnergy() const {
return mCurrentData.midEnergy;
}
float AudioReactive::getTrebleEnergy() const {
return mCurrentData.trebleEnergy;
}
fl::u8 AudioReactive::volumeToScale255() const {
float vol = (mCurrentData.volume < 0.0f) ? 0.0f : ((mCurrentData.volume > 255.0f) ? 255.0f : mCurrentData.volume);
return static_cast<fl::u8>(vol);
}
CRGB AudioReactive::volumeToColor(const CRGBPalette16& /* palette */) const {
fl::u8 index = volumeToScale255();
// Simplified color palette lookup
return CRGB(index, index, index); // For now, return grayscale
}
fl::u8 AudioReactive::frequencyToScale255(fl::u8 binIndex) const {
if (binIndex >= 16) return 0;
float value = (mCurrentData.frequencyBins[binIndex] < 0.0f) ? 0.0f :
((mCurrentData.frequencyBins[binIndex] > 255.0f) ? 255.0f : mCurrentData.frequencyBins[binIndex]);
return static_cast<fl::u8>(value);
}
// Enhanced beat detection methods
void AudioReactive::calculateBandEnergies() {
// Calculate energy for bass frequencies (bins 0-1)
mCurrentData.bassEnergy = (mCurrentData.frequencyBins[0] + mCurrentData.frequencyBins[1]) / 2.0f;
// Calculate energy for mid frequencies (bins 6-7)
mCurrentData.midEnergy = (mCurrentData.frequencyBins[6] + mCurrentData.frequencyBins[7]) / 2.0f;
// Calculate energy for treble frequencies (bins 14-15)
mCurrentData.trebleEnergy = (mCurrentData.frequencyBins[14] + mCurrentData.frequencyBins[15]) / 2.0f;
}
void AudioReactive::updateSpectralFlux() {
if (!mSpectralFluxDetector) {
mCurrentData.spectralFlux = 0.0f;
return;
}
// Calculate spectral flux from current and previous frequency bins
mCurrentData.spectralFlux = mSpectralFluxDetector->calculateSpectralFlux(
mCurrentData.frequencyBins,
mPreviousMagnitudes.data()
);
// Update previous magnitudes for next frame
for (int i = 0; i < 16; ++i) {
mPreviousMagnitudes[i] = mCurrentData.frequencyBins[i];
}
}
void AudioReactive::detectEnhancedBeats(fl::u32 currentTimeMs) {
// Reset beat flags
mCurrentData.bassBeatDetected = false;
mCurrentData.midBeatDetected = false;
mCurrentData.trebleBeatDetected = false;
// Skip if enhanced beat detection is disabled
if (!mConfig.enableSpectralFlux && !mConfig.enableMultiBand) {
return;
}
// Need minimum time since last beat for enhanced detection too
if (currentTimeMs - mLastBeatTime < BEAT_COOLDOWN) {
return;
}
// Spectral flux-based beat detection
if (mConfig.enableSpectralFlux && mSpectralFluxDetector) {
bool onsetDetected = mSpectralFluxDetector->detectOnset(
mCurrentData.frequencyBins,
mPreviousMagnitudes.data()
);
if (onsetDetected) {
// Enhance the traditional beat detection when spectral flux confirms
mCurrentData.beatDetected = true;
mLastBeatTime = currentTimeMs;
}
}
// Multi-band beat detection
if (mConfig.enableMultiBand) {
// Bass beat detection (bins 0-1)
if (mCurrentData.bassEnergy > mConfig.bassThreshold) {
mCurrentData.bassBeatDetected = true;
}
// Mid beat detection (bins 6-7)
if (mCurrentData.midEnergy > mConfig.midThreshold) {
mCurrentData.midBeatDetected = true;
}
// Treble beat detection (bins 14-15)
if (mCurrentData.trebleEnergy > mConfig.trebleThreshold) {
mCurrentData.trebleBeatDetected = true;
}
}
}
void AudioReactive::applyPerceptualWeighting() {
// Apply perceptual weighting if available
if (mPerceptualWeighting) {
mPerceptualWeighting->applyAWeighting(mCurrentData);
// Apply loudness compensation with reference level of 50.0f
mPerceptualWeighting->applyLoudnessCompensation(mCurrentData, 50.0f);
}
}
// Helper methods
float AudioReactive::mapFrequencyBin(int fromBin, int toBin) {
if (fromBin < 0 || toBin >= static_cast<int>(mFFTBins.size()) || fromBin > toBin) {
return 0.0f;
}
float sum = 0.0f;
for (int i = fromBin; i <= toBin; ++i) {
if (i < static_cast<int>(mFFTBins.bins_raw.size())) {
sum += mFFTBins.bins_raw[i];
}
}
return sum / static_cast<float>(toBin - fromBin + 1);
}
float AudioReactive::computeRMS(const fl::vector<fl::i16>& samples) {
if (samples.empty()) return 0.0f;
float sumSquares = 0.0f;
for (const auto& sample : samples) {
float f = static_cast<float>(sample);
sumSquares += f * f;
}
return sqrtf(sumSquares / samples.size());
}
// SpectralFluxDetector implementation
SpectralFluxDetector::SpectralFluxDetector()
: mFluxThreshold(0.1f)
#if SKETCH_HAS_LOTS_OF_MEMORY
, mHistoryIndex(0)
#endif
{
// Initialize previous magnitudes to zero
for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
mPreviousMagnitudes[i] = 0.0f;
}
#if SKETCH_HAS_LOTS_OF_MEMORY
// Initialize flux history to zero
for (fl::size i = 0; i < mFluxHistory.size(); ++i) {
mFluxHistory[i] = 0.0f;
}
#endif
}
SpectralFluxDetector::~SpectralFluxDetector() = default;
void SpectralFluxDetector::reset() {
for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
mPreviousMagnitudes[i] = 0.0f;
}
#if SKETCH_HAS_LOTS_OF_MEMORY
for (fl::size i = 0; i < mFluxHistory.size(); ++i) {
mFluxHistory[i] = 0.0f;
}
mHistoryIndex = 0;
#endif
}
bool SpectralFluxDetector::detectOnset(const float* currentBins, const float* /* previousBins */) {
float flux = calculateSpectralFlux(currentBins, mPreviousMagnitudes.data());
#if SKETCH_HAS_LOTS_OF_MEMORY
// Store flux in history for adaptive threshold calculation
mFluxHistory[mHistoryIndex] = flux;
mHistoryIndex = (mHistoryIndex + 1) % mFluxHistory.size();
float adaptiveThreshold = calculateAdaptiveThreshold();
return flux > adaptiveThreshold;
#else
// Simple fixed threshold for memory-constrained platforms
return flux > mFluxThreshold;
#endif
}
float SpectralFluxDetector::calculateSpectralFlux(const float* currentBins, const float* previousBins) {
float flux = 0.0f;
// Calculate spectral flux as sum of positive differences
for (int i = 0; i < 16; ++i) {
float diff = currentBins[i] - previousBins[i];
if (diff > 0.0f) {
flux += diff;
}
}
// Update previous magnitudes for next calculation
for (int i = 0; i < 16; ++i) {
mPreviousMagnitudes[i] = currentBins[i];
}
return flux;
}
void SpectralFluxDetector::setThreshold(float threshold) {
mFluxThreshold = threshold;
}
float SpectralFluxDetector::getThreshold() const {
return mFluxThreshold;
}
#if SKETCH_HAS_LOTS_OF_MEMORY
float SpectralFluxDetector::calculateAdaptiveThreshold() {
// Calculate moving average of flux history
float sum = 0.0f;
for (fl::size i = 0; i < mFluxHistory.size(); ++i) {
sum += mFluxHistory[i];
}
float average = sum / mFluxHistory.size();
// Adaptive threshold is base threshold plus some multiple of recent average
return mFluxThreshold + (average * 0.5f);
}
#endif
// BeatDetectors implementation
BeatDetectors::BeatDetectors()
: mBassEnergy(0.0f), mMidEnergy(0.0f), mTrebleEnergy(0.0f)
, mPreviousBassEnergy(0.0f), mPreviousMidEnergy(0.0f), mPreviousTrebleEnergy(0.0f)
{
}
BeatDetectors::~BeatDetectors() = default;
void BeatDetectors::reset() {
#if SKETCH_HAS_LOTS_OF_MEMORY
bass.reset();
mid.reset();
treble.reset();
#else
combined.reset();
#endif
mBassEnergy = 0.0f;
mMidEnergy = 0.0f;
mTrebleEnergy = 0.0f;
mPreviousBassEnergy = 0.0f;
mPreviousMidEnergy = 0.0f;
mPreviousTrebleEnergy = 0.0f;
}
void BeatDetectors::detectBeats(const float* frequencyBins, AudioData& audioData) {
// Calculate current band energies
mBassEnergy = (frequencyBins[0] + frequencyBins[1]) / 2.0f;
mMidEnergy = (frequencyBins[6] + frequencyBins[7]) / 2.0f;
mTrebleEnergy = (frequencyBins[14] + frequencyBins[15]) / 2.0f;
#if SKETCH_HAS_LOTS_OF_MEMORY
// Use separate detectors for each band
audioData.bassBeatDetected = bass.detectOnset(&mBassEnergy, &mPreviousBassEnergy);
audioData.midBeatDetected = mid.detectOnset(&mMidEnergy, &mPreviousMidEnergy);
audioData.trebleBeatDetected = treble.detectOnset(&mTrebleEnergy, &mPreviousTrebleEnergy);
#else
// Use simple threshold detection for memory-constrained platforms
audioData.bassBeatDetected = (mBassEnergy > mPreviousBassEnergy * 1.3f) && (mBassEnergy > 0.1f);
audioData.midBeatDetected = (mMidEnergy > mPreviousMidEnergy * 1.25f) && (mMidEnergy > 0.08f);
audioData.trebleBeatDetected = (mTrebleEnergy > mPreviousTrebleEnergy * 1.2f) && (mTrebleEnergy > 0.05f);
#endif
// Update previous energies
mPreviousBassEnergy = mBassEnergy;
mPreviousMidEnergy = mMidEnergy;
mPreviousTrebleEnergy = mTrebleEnergy;
}
void BeatDetectors::setThresholds(float bassThresh, float midThresh, float trebleThresh) {
#if SKETCH_HAS_LOTS_OF_MEMORY
bass.setThreshold(bassThresh);
mid.setThreshold(midThresh);
treble.setThreshold(trebleThresh);
#else
combined.setThreshold((bassThresh + midThresh + trebleThresh) / 3.0f);
#endif
}
// PerceptualWeighting implementation
PerceptualWeighting::PerceptualWeighting()
#if SKETCH_HAS_LOTS_OF_MEMORY
: mHistoryIndex(0)
#endif
{
#if SKETCH_HAS_LOTS_OF_MEMORY
// Initialize loudness history to zero
for (fl::size i = 0; i < mLoudnessHistory.size(); ++i) {
mLoudnessHistory[i] = 0.0f;
}
// Suppress unused warning until mHistoryIndex is implemented
(void)mHistoryIndex;
#endif
}
PerceptualWeighting::~PerceptualWeighting() = default;
void PerceptualWeighting::applyAWeighting(AudioData& data) const {
// Apply A-weighting coefficients to frequency bins
for (int i = 0; i < 16; ++i) {
data.frequencyBins[i] *= A_WEIGHTING_COEFFS[i];
}
}
void PerceptualWeighting::applyLoudnessCompensation(AudioData& data, float referenceLevel) const {
// Calculate current loudness level
float currentLoudness = data.volume;
// Calculate compensation factor based on difference from reference
float compensationFactor = 1.0f;
if (currentLoudness < referenceLevel) {
// Boost quiet signals
compensationFactor = 1.0f + (referenceLevel - currentLoudness) / referenceLevel * 0.3f;
} else if (currentLoudness > referenceLevel * 1.5f) {
// Slightly reduce very loud signals
compensationFactor = 1.0f - (currentLoudness - referenceLevel * 1.5f) / (referenceLevel * 2.0f) * 0.2f;
}
// Apply compensation to frequency bins
for (int i = 0; i < 16; ++i) {
data.frequencyBins[i] *= compensationFactor;
}
#if SKETCH_HAS_LOTS_OF_MEMORY
// Store in history for future adaptive compensation (not implemented yet)
// This would be used for more sophisticated dynamic range compensation
#endif
}
} // namespace fl

View File

@@ -0,0 +1,231 @@
#pragma once
#include "fl/fft.h"
#include "fl/math.h"
#include "fl/vector.h"
#include "fl/stdint.h"
#include "fl/int.h"
#include "fl/audio.h"
#include "fl/array.h"
#include "fl/unique_ptr.h"
#include "fl/sketch_macros.h"
#include "crgb.h"
#include "fl/colorutils.h"
namespace fl {
// Forward declarations for enhanced beat detection
class SpectralFluxDetector;
class PerceptualWeighting;
// Audio data structure - matches original WLED output with extensions
struct AudioData {
float volume = 0.0f; // Overall volume level (0-255)
float volumeRaw = 0.0f; // Raw volume without smoothing
float peak = 0.0f; // Peak level (0-255)
bool beatDetected = false; // Beat detection flag
float frequencyBins[16] = {0}; // 16 frequency bins (matches WLED NUM_GEQ_CHANNELS)
float dominantFrequency = 0.0f; // Major peak frequency (Hz)
float magnitude = 0.0f; // FFT magnitude of dominant frequency
fl::u32 timestamp = 0; // millis() when data was captured
// Enhanced beat detection fields
bool bassBeatDetected = false; // Bass-specific beat detection
bool midBeatDetected = false; // Mid-range beat detection
bool trebleBeatDetected = false; // Treble beat detection
float spectralFlux = 0.0f; // Current spectral flux value
float bassEnergy = 0.0f; // Energy in bass frequencies (0-1)
float midEnergy = 0.0f; // Energy in mid frequencies (6-7)
float trebleEnergy = 0.0f; // Energy in treble frequencies (14-15)
};
struct AudioReactiveConfig {
fl::u8 gain = 128; // Input gain (0-255)
fl::u8 sensitivity = 128; // AGC sensitivity
bool agcEnabled = true; // Auto gain control
bool noiseGate = true; // Noise gate
fl::u8 attack = 50; // Attack time (ms) - how fast to respond to increases
fl::u8 decay = 200; // Decay time (ms) - how slow to respond to decreases
u16 sampleRate = 22050; // Sample rate (Hz)
fl::u8 scalingMode = 3; // 0=none, 1=log, 2=linear, 3=sqrt
// Enhanced beat detection configuration
bool enableSpectralFlux = true; // Enable spectral flux-based beat detection
bool enableMultiBand = true; // Enable multi-band beat detection
float spectralFluxThreshold = 0.1f; // Threshold for spectral flux detection
float bassThreshold = 0.15f; // Threshold for bass beat detection
float midThreshold = 0.12f; // Threshold for mid beat detection
float trebleThreshold = 0.08f; // Threshold for treble beat detection
};
class AudioReactive {
public:
AudioReactive();
~AudioReactive();
// Setup
void begin(const AudioReactiveConfig& config = AudioReactiveConfig{});
void setConfig(const AudioReactiveConfig& config);
// Process audio sample - this does all the work immediately
void processSample(const AudioSample& sample);
// Optional: update smoothing without new sample data
void update(fl::u32 currentTimeMs);
// Data access
const AudioData& getData() const;
const AudioData& getSmoothedData() const;
// Convenience accessors
float getVolume() const;
float getBass() const; // Average of bins 0-1
float getMid() const; // Average of bins 6-7
float getTreble() const; // Average of bins 14-15
bool isBeat() const;
// Enhanced beat detection accessors
bool isBassBeat() const;
bool isMidBeat() const;
bool isTrebleBeat() const;
float getSpectralFlux() const;
float getBassEnergy() const;
float getMidEnergy() const;
float getTrebleEnergy() const;
// Effect helpers
fl::u8 volumeToScale255() const;
CRGB volumeToColor(const CRGBPalette16& palette) const;
fl::u8 frequencyToScale255(fl::u8 binIndex) const;
private:
// Internal processing methods
void processFFT(const AudioSample& sample);
void mapFFTBinsToFrequencyChannels();
void updateVolumeAndPeak(const AudioSample& sample);
void detectBeat(fl::u32 currentTimeMs);
void smoothResults();
void applyScaling();
void applyGain();
// Enhanced beat detection methods
void detectEnhancedBeats(fl::u32 currentTimeMs);
void calculateBandEnergies();
void updateSpectralFlux();
void applyPerceptualWeighting();
// Helper methods
float mapFrequencyBin(int fromBin, int toBin);
float computeRMS(const fl::vector<fl::i16>& samples);
// Configuration
AudioReactiveConfig mConfig;
// FFT processing
FFT mFFT;
FFTBins mFFTBins;
// Audio data
AudioData mCurrentData;
AudioData mSmoothedData;
// Processing state
fl::u32 mLastBeatTime = 0;
static constexpr fl::u32 BEAT_COOLDOWN = 100; // 100ms minimum between beats
// Volume tracking for beat detection
float mPreviousVolume = 0.0f;
float mVolumeThreshold = 10.0f;
// Pink noise compensation (from WLED)
static constexpr float PINK_NOISE_COMPENSATION[16] = {
1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f,
1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f
};
// AGC state
float mAGCMultiplier = 1.0f;
float mMaxSample = 0.0f;
float mAverageLevel = 0.0f;
// Enhanced beat detection components
fl::unique_ptr<SpectralFluxDetector> mSpectralFluxDetector;
fl::unique_ptr<PerceptualWeighting> mPerceptualWeighting;
// Enhanced beat detection state
fl::array<float, 16> mPreviousMagnitudes;
};
// Spectral flux-based onset detection for enhanced beat detection
class SpectralFluxDetector {
public:
SpectralFluxDetector();
~SpectralFluxDetector();
void reset();
bool detectOnset(const float* currentBins, const float* previousBins);
float calculateSpectralFlux(const float* currentBins, const float* previousBins);
void setThreshold(float threshold);
float getThreshold() const;
private:
float mFluxThreshold;
fl::array<float, 16> mPreviousMagnitudes;
#if SKETCH_HAS_LOTS_OF_MEMORY
fl::array<float, 32> mFluxHistory; // For advanced smoothing
fl::size mHistoryIndex;
float calculateAdaptiveThreshold();
#endif
};
// Multi-band beat detection for different frequency ranges
struct BeatDetectors {
BeatDetectors();
~BeatDetectors();
void reset();
void detectBeats(const float* frequencyBins, AudioData& audioData);
void setThresholds(float bassThresh, float midThresh, float trebleThresh);
private:
#if SKETCH_HAS_LOTS_OF_MEMORY
SpectralFluxDetector bass; // 20-200 Hz (bins 0-1)
SpectralFluxDetector mid; // 200-2000 Hz (bins 6-7)
SpectralFluxDetector treble; // 2000-20000 Hz (bins 14-15)
#else
SpectralFluxDetector combined; // Single detector for memory-constrained
#endif
// Energy tracking for band-specific thresholds
float mBassEnergy;
float mMidEnergy;
float mTrebleEnergy;
float mPreviousBassEnergy;
float mPreviousMidEnergy;
float mPreviousTrebleEnergy;
};
// Perceptual audio weighting for psychoacoustic processing
class PerceptualWeighting {
public:
PerceptualWeighting();
~PerceptualWeighting();
void applyAWeighting(AudioData& data) const;
void applyLoudnessCompensation(AudioData& data, float referenceLevel) const;
private:
// A-weighting coefficients for 16-bin frequency analysis
static constexpr float A_WEIGHTING_COEFFS[16] = {
0.5f, 0.6f, 0.8f, 1.0f, 1.2f, 1.3f, 1.4f, 1.4f,
1.3f, 1.2f, 1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f
};
#if SKETCH_HAS_LOTS_OF_MEMORY
fl::array<float, 16> mLoudnessHistory; // For dynamic compensation
fl::size mHistoryIndex;
#endif
};
} // namespace fl

View File

@@ -0,0 +1,8 @@
#pragma once
#ifdef __AVR__
#define AVR_DISALLOWED \
[[deprecated("This function or class is deprecated on AVR.")]]
#else
#define AVR_DISALLOWED
#endif

View File

@@ -0,0 +1,74 @@
#pragma once
// What is bit cast?
// Bit cast is a safe version of reinterpret_cast that is robust against strict aliasing rules
// that are used in aggressive compiler optimizations.
#include "fl/type_traits.h"
#include "fl/int.h"
namespace fl {
//-------------------------------------------------------------------------------
// bit_cast - Safe type-punning utility (C++20 std::bit_cast equivalent)
//-------------------------------------------------------------------------------
// Helper trait for bitcast - check if a type can be bitcast (relax POD requirement)
template <typename T>
struct is_bitcast_compatible {
static constexpr bool value = fl::is_integral<T>::value ||
fl::is_floating_point<T>::value ||
fl::is_pod<T>::value;
};
// Specializations for const types
template <typename T>
struct is_bitcast_compatible<const T> {
static constexpr bool value = is_bitcast_compatible<T>::value;
};
// Specializations for pointer types
template <typename T>
struct is_bitcast_compatible<T*> {
static constexpr bool value = true;
};
// C++20-style bit_cast for safe type reinterpretation
// Uses union for zero-cost type punning - compiler optimizes to direct assignment
template <typename To, typename From>
To bit_cast(const From& from) noexcept {
static_assert(sizeof(To) == sizeof(From), "bit_cast: types must have the same size");
static_assert(is_bitcast_compatible<To>::value, "bit_cast: destination type must be bitcast compatible");
static_assert(is_bitcast_compatible<From>::value, "bit_cast: source type must be bitcast compatible");
union { // robust against strict aliasing rules
From from_val;
To to_val;
} u;
u.from_val = from;
return u.to_val;
}
// Overload for pointer types - converts storage pointer to typed pointer safely
template <typename To>
To* bit_cast_ptr(void* storage) noexcept {
return bit_cast<To*>(storage);
}
template <typename To>
const To* bit_cast_ptr(const void* storage) noexcept {
return bit_cast<const To*>(storage);
}
// Additional utility for uptr conversions (common pattern in the codebase)
template <typename T>
uptr ptr_to_int(T* ptr) noexcept {
return bit_cast<uptr>(ptr);
}
template <typename T>
T* int_to_ptr(uptr value) noexcept {
return bit_cast<T*>(value);
}
} // namespace fl

View File

@@ -0,0 +1,28 @@
#include "fl/bitset.h"
#include "fl/string.h"
namespace fl {
namespace detail {
void to_string(const fl::u16 *bit_data, fl::u32 bit_count, string* dst) {
fl::string& result = *dst;
constexpr fl::u32 bits_per_block = 8 * sizeof(fl::u16); // 16 bits per block
for (fl::u32 i = 0; i < bit_count; ++i) {
const fl::u32 block_idx = i / bits_per_block;
const fl::u32 bit_offset = i % bits_per_block;
// Extract the bit from the block
bool bit_value = (bit_data[block_idx] >> bit_offset) & 1;
result.append(bit_value ? "1" : "0");
}
}
} // namespace detail
// Implementation for bitset_dynamic::to_string
void bitset_dynamic::to_string(string* dst) const {
detail::to_string(_blocks, _size, dst);
}
} // namespace fl

View File

@@ -0,0 +1,663 @@
#pragma once
#include "fl/bitset_dynamic.h"
#include "fl/type_traits.h"
#include "fl/variant.h"
#include "fl/stdint.h"
#include "fl/int.h"
#include "fl/compiler_control.h"
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING(double-promotion)
FL_DISABLE_WARNING(float-conversion)
FL_DISABLE_WARNING(sign-conversion)
FL_DISABLE_WARNING_IMPLICIT_INT_CONVERSION
FL_DISABLE_WARNING_FLOAT_CONVERSION
namespace fl {
template <fl::u32 N> class BitsetInlined;
template <fl::u32 N> class BitsetFixed;
class string;
template <fl::u32 N = 16>
using bitset = BitsetInlined<N>; // inlined but can go bigger.
template <fl::u32 N>
using bitset_fixed = BitsetFixed<N>; // fixed size, no dynamic allocation.
// TODO: move this to fl/math.h
template<typename IntType>
inline fl::u8 popcount(IntType value) {
return static_cast<fl::u8>(__builtin_popcount(value));
}
template<typename IntType>
inline fl::u8 countr_zero(IntType value) {
return static_cast<fl::u8>(__builtin_ctz(value));
}
/// A simple fixed-size Bitset implementation similar to std::Bitset.
template <fl::u32 N> class BitsetFixed {
private:
static_assert(sizeof(fl::u16) == 2, "u16 should be 2 bytes");
static constexpr fl::u32 bits_per_block = 8 * sizeof(fl::u16);
static constexpr fl::u32 block_count =
(N + bits_per_block - 1) / bits_per_block;
using block_type = fl::u16;
// Underlying blocks storing bits
block_type _blocks[block_count];
public:
struct Proxy {
BitsetFixed &_bitset;
fl::u32 _pos;
Proxy(BitsetFixed &bitset, fl::u32 pos) : _bitset(bitset), _pos(pos) {}
Proxy &operator=(bool value) {
_bitset.set(_pos, value);
return *this;
}
operator bool() const { return _bitset.test(_pos); }
};
Proxy operator[](fl::u32 pos) { return Proxy(*this, pos); }
/// Constructs a BitsetFixed with all bits reset.
constexpr BitsetFixed() noexcept : _blocks{} {}
void to_string(string* dst) const {
detail::to_string(_blocks, N, dst);
}
/// Resets all bits to zero.
void reset() noexcept {
for (fl::u32 i = 0; i < block_count; ++i) {
_blocks[i] = 0;
}
}
/// Sets or clears the bit at position pos.
BitsetFixed &set(fl::u32 pos, bool value = true) {
if (pos < N) {
const fl::u32 idx = pos / bits_per_block;
const fl::u32 off = pos % bits_per_block;
if (value) {
_blocks[idx] |= (block_type(1) << off);
} else {
_blocks[idx] &= ~(block_type(1) << off);
}
}
return *this;
}
void assign(fl::size n, bool value) {
if (n > N) {
n = N;
}
for (fl::size i = 0; i < n; ++i) {
set(i, value);
}
}
/// Clears the bit at position pos.
BitsetFixed &reset(fl::u32 pos) { return set(pos, false); }
/// Flips (toggles) the bit at position pos.
BitsetFixed &flip(fl::u32 pos) {
if (pos < N) {
const fl::u32 idx = pos / bits_per_block;
const fl::u32 off = pos % bits_per_block;
_blocks[idx] ^= (block_type(1) << off);
}
return *this;
}
/// Flips all bits.
BitsetFixed &flip() noexcept {
for (fl::u32 i = 0; i < block_count; ++i) {
_blocks[i] = ~_blocks[i];
}
// Mask out unused high bits in the last block
if (N % bits_per_block != 0) {
const fl::u32 extra = bits_per_block - (N % bits_per_block);
_blocks[block_count - 1] &= (~block_type(0) >> extra);
}
return *this;
}
/// Tests whether the bit at position pos is set.
bool test(fl::u32 pos) const noexcept {
if (pos < N) {
const fl::u32 idx = pos / bits_per_block;
const fl::u32 off = pos % bits_per_block;
return (_blocks[idx] >> off) & 1;
}
return false;
}
/// Returns the value of the bit at position pos.
bool operator[](fl::u32 pos) const noexcept { return test(pos); }
/// Returns the number of set bits.
fl::u32 count() const noexcept {
fl::u32 cnt = 0;
// Count bits in all complete blocks
for (fl::u32 i = 0; i < block_count - 1; ++i) {
cnt += fl::popcount(_blocks[i]);
}
// For the last block, we need to be careful about counting only valid
// bits
if (block_count > 0) {
block_type last_block = _blocks[block_count - 1];
// If N is not a multiple of bits_per_block, mask out the unused
// bits
if (N % bits_per_block != 0) {
const fl::u32 valid_bits = N % bits_per_block;
// Create a mask with only the valid bits set to 1
block_type mask = (valid_bits == bits_per_block)
? static_cast<block_type>(~block_type(0))
: ((block_type(1) << valid_bits) - 1);
last_block &= mask;
}
cnt += fl::popcount(last_block);
}
return cnt;
}
/// Queries.
bool any() const noexcept { return count() > 0; }
bool none() const noexcept { return count() == 0; }
bool all() const noexcept {
if (N == 0)
return true;
// Check all complete blocks
for (fl::u32 i = 0; i < block_count - 1; ++i) {
if (_blocks[i] != ~block_type(0)) {
return false;
}
}
// Check the last block
if (block_count > 0) {
block_type mask;
if (N % bits_per_block != 0) {
// Create a mask for the valid bits in the last block
mask = (block_type(1) << (N % bits_per_block)) - 1;
} else {
mask = static_cast<block_type>(~block_type(0));
}
if ((_blocks[block_count - 1] & mask) != mask) {
return false;
}
}
return true;
}
/// Bitwise AND
BitsetFixed &operator&=(const BitsetFixed &other) noexcept {
for (fl::u32 i = 0; i < block_count; ++i) {
_blocks[i] &= other._blocks[i];
}
return *this;
}
/// Bitwise OR
BitsetFixed &operator|=(const BitsetFixed &other) noexcept {
for (fl::u32 i = 0; i < block_count; ++i) {
_blocks[i] |= other._blocks[i];
}
return *this;
}
/// Bitwise XOR
BitsetFixed &operator^=(const BitsetFixed &other) noexcept {
for (fl::u32 i = 0; i < block_count; ++i) {
_blocks[i] ^= other._blocks[i];
}
return *this;
}
/// Size of the BitsetFixed (number of bits).
constexpr fl::u32 size() const noexcept { return N; }
/// Finds the first bit that matches the test value.
/// Returns the index of the first matching bit, or -1 if none found.
/// @param test_value The value to search for (true or false)
/// @param offset Starting position to search from (default: 0)
fl::i32 find_first(bool test_value, fl::u32 offset = 0) const noexcept {
// If offset is beyond our size, no match possible
if (offset >= N) {
return -1;
}
// Calculate which block to start from
fl::u32 start_block = offset / bits_per_block;
fl::u32 start_bit = offset % bits_per_block;
for (fl::u32 block_idx = start_block; block_idx < block_count; ++block_idx) {
block_type current_block = _blocks[block_idx];
// For the last block, we need to mask out unused bits
if (block_idx == block_count - 1 && N % bits_per_block != 0) {
const fl::u32 valid_bits = N % bits_per_block;
block_type mask = (valid_bits == bits_per_block)
? static_cast<block_type>(~block_type(0))
: ((block_type(1) << valid_bits) - 1);
current_block &= mask;
}
// If looking for false bits, invert the block
if (!test_value) {
current_block = ~current_block;
}
// For the first block, mask out bits before the offset
if (block_idx == start_block && start_bit > 0) {
current_block &= ~((block_type(1) << start_bit) - 1);
}
// If there are any matching bits in this block
if (current_block != 0) {
// Find the first set bit
fl::u32 bit_pos = fl::countr_zero(current_block);
fl::u32 absolute_pos = block_idx * bits_per_block + bit_pos;
// Make sure we haven't gone past the end of the bitset
if (absolute_pos < N) {
return static_cast<fl::i32>(absolute_pos);
}
}
}
return -1; // No matching bit found
}
/// Finds the first run of consecutive bits that match the test value.
/// Returns the index of the first bit in the run, or -1 if no run found.
/// @param test_value The value to search for (true or false)
/// @param min_length Minimum length of the run (default: 1)
/// @param offset Starting position to search from (default: 0)
fl::i32 find_run(bool test_value, fl::u32 min_length, fl::u32 offset = 0) const noexcept {
fl::u32 run_start = offset;
fl::u32 run_length = 0;
for (fl::u32 i = offset; i < N && run_length < min_length; ++i) {
bool current_bit = test(i);
if (current_bit != test_value) {
run_length = 0;
if (i + 1 < N) {
run_start = i + 1;
}
} else {
++run_length;
}
}
if (run_length >= min_length) {
return static_cast<fl::i32>(run_start);
}
return -1; // No run found
}
/// Friend operators for convenience.
friend BitsetFixed operator&(BitsetFixed lhs,
const BitsetFixed &rhs) noexcept {
return lhs &= rhs;
}
friend BitsetFixed operator|(BitsetFixed lhs,
const BitsetFixed &rhs) noexcept {
return lhs |= rhs;
}
friend BitsetFixed operator^(BitsetFixed lhs,
const BitsetFixed &rhs) noexcept {
return lhs ^= rhs;
}
friend BitsetFixed operator~(BitsetFixed bs) noexcept { return bs.flip(); }
};
/// A Bitset implementation with inline storage that can grow if needed.
/// T is the storage type (u8, u16, u32, uint64_t)
/// N is the initial number of bits to store inline
template <fl::u32 N = 16> // Default size is 16 bits, or 2 bytes
class BitsetInlined {
private:
// Either store a fixed Bitset<N> or a dynamic Bitset
using fixed_bitset = BitsetFixed<N>;
Variant<fixed_bitset, bitset_dynamic> _storage;
public:
struct Proxy {
BitsetInlined &_bitset;
fl::u32 _pos;
Proxy(BitsetInlined &bitset, fl::u32 pos)
: _bitset(bitset), _pos(pos) {}
Proxy &operator=(bool value) {
_bitset.set(_pos, value);
return *this;
}
operator bool() const { return _bitset.test(_pos); }
};
Proxy operator[](fl::u32 pos) { return Proxy(*this, pos); }
/// Constructs a Bitset with all bits reset.
BitsetInlined() : _storage(fixed_bitset()) {}
BitsetInlined(fl::size size) : _storage(fixed_bitset()) {
if (size > N) {
_storage = bitset_dynamic(size);
}
}
BitsetInlined(const BitsetInlined &other) : _storage(other._storage) {}
BitsetInlined(BitsetInlined &&other) noexcept
: _storage(fl::move(other._storage)) {}
BitsetInlined &operator=(const BitsetInlined &other) {
if (this != &other) {
_storage = other._storage;
}
return *this;
}
BitsetInlined &operator=(BitsetInlined &&other) noexcept {
if (this != &other) {
_storage = fl::move(other._storage);
}
return *this;
}
/// Resets all bits to zero.
void reset() noexcept {
if (_storage.template is<fixed_bitset>()) {
_storage.template ptr<fixed_bitset>()->reset();
} else {
_storage.template ptr<bitset_dynamic>()->reset();
}
}
void assign(fl::size n, bool value) {
resize(n);
if (_storage.template is<fixed_bitset>()) {
_storage.template ptr<fixed_bitset>()->assign(n, value);
} else {
_storage.template ptr<bitset_dynamic>()->assign(n, value);
}
}
/// Resizes the Bitset if needed
void resize(fl::u32 new_size) {
if (new_size <= N) {
// If we're already using the fixed Bitset, nothing to do
if (_storage.template is<bitset_dynamic>()) {
// Convert back to fixed Bitset
fixed_bitset fixed;
bitset_dynamic *dynamic =
_storage.template ptr<bitset_dynamic>();
// Copy bits from dynamic to fixed
for (fl::u32 i = 0; i < N && i < dynamic->size(); ++i) {
if (dynamic->test(i)) {
fixed.set(i);
}
}
_storage = fixed;
}
} else {
// Need to use dynamic Bitset
if (_storage.template is<fixed_bitset>()) {
// Convert from fixed to dynamic
bitset_dynamic dynamic(new_size);
fixed_bitset *fixed = _storage.template ptr<fixed_bitset>();
// Copy bits from fixed to dynamic
for (fl::u32 i = 0; i < N; ++i) {
if (fixed->test(i)) {
dynamic.set(i);
}
}
_storage = dynamic;
} else {
// Already using dynamic, just resize
_storage.template ptr<bitset_dynamic>()->resize(new_size);
}
}
}
/// Sets or clears the bit at position pos.
BitsetInlined &set(fl::u32 pos, bool value = true) {
if (pos >= N && _storage.template is<fixed_bitset>()) {
resize(pos + 1);
}
if (_storage.template is<fixed_bitset>()) {
if (pos < N) {
_storage.template ptr<fixed_bitset>()->set(pos, value);
}
} else {
if (pos >= _storage.template ptr<bitset_dynamic>()->size()) {
_storage.template ptr<bitset_dynamic>()->resize(pos + 1);
}
_storage.template ptr<bitset_dynamic>()->set(pos, value);
}
return *this;
}
/// Clears the bit at position pos.
BitsetInlined &reset(fl::u32 pos) { return set(pos, false); }
/// Flips (toggles) the bit at position pos.
BitsetInlined &flip(fl::u32 pos) {
if (pos >= N && _storage.template is<fixed_bitset>()) {
resize(pos + 1);
}
if (_storage.template is<fixed_bitset>()) {
if (pos < N) {
_storage.template ptr<fixed_bitset>()->flip(pos);
}
} else {
if (pos >= _storage.template ptr<bitset_dynamic>()->size()) {
_storage.template ptr<bitset_dynamic>()->resize(pos + 1);
}
_storage.template ptr<bitset_dynamic>()->flip(pos);
}
return *this;
}
/// Flips all bits.
BitsetInlined &flip() noexcept {
if (_storage.template is<fixed_bitset>()) {
_storage.template ptr<fixed_bitset>()->flip();
} else {
_storage.template ptr<bitset_dynamic>()->flip();
}
return *this;
}
/// Tests whether the bit at position pos is set.
bool test(fl::u32 pos) const noexcept {
if (_storage.template is<fixed_bitset>()) {
return pos < N ? _storage.template ptr<fixed_bitset>()->test(pos)
: false;
} else {
return _storage.template ptr<bitset_dynamic>()->test(pos);
}
}
/// Returns the value of the bit at position pos.
bool operator[](fl::u32 pos) const noexcept { return test(pos); }
/// Returns the number of set bits.
fl::u32 count() const noexcept {
if (_storage.template is<fixed_bitset>()) {
return _storage.template ptr<fixed_bitset>()->count();
} else {
return _storage.template ptr<bitset_dynamic>()->count();
}
}
/// Queries.
bool any() const noexcept {
if (_storage.template is<fixed_bitset>()) {
return _storage.template ptr<fixed_bitset>()->any();
} else {
return _storage.template ptr<bitset_dynamic>()->any();
}
}
bool none() const noexcept {
if (_storage.template is<fixed_bitset>()) {
return _storage.template ptr<fixed_bitset>()->none();
} else {
return _storage.template ptr<bitset_dynamic>()->none();
}
}
bool all() const noexcept {
if (_storage.template is<fixed_bitset>()) {
return _storage.template ptr<fixed_bitset>()->all();
} else {
return _storage.template ptr<bitset_dynamic>()->all();
}
}
/// Size of the Bitset (number of bits).
fl::u32 size() const noexcept {
if (_storage.template is<fixed_bitset>()) {
return N;
} else {
return _storage.template ptr<bitset_dynamic>()->size();
}
}
/// Convert bitset to string representation
void to_string(string* dst) const {
if (_storage.template is<fixed_bitset>()) {
_storage.template ptr<fixed_bitset>()->to_string(dst);
} else {
_storage.template ptr<bitset_dynamic>()->to_string(dst);
}
}
/// Finds the first bit that matches the test value.
/// Returns the index of the first matching bit, or -1 if none found.
/// @param test_value The value to search for (true or false)
/// @param offset Starting position to search from (default: 0)
fl::i32 find_first(bool test_value, fl::u32 offset = 0) const noexcept {
if (_storage.template is<fixed_bitset>()) {
return _storage.template ptr<fixed_bitset>()->find_first(test_value, offset);
} else {
return _storage.template ptr<bitset_dynamic>()->find_first(test_value, offset);
}
}
/// Bitwise operators
friend BitsetInlined operator~(const BitsetInlined &bs) noexcept {
BitsetInlined result = bs;
result.flip();
return result;
}
friend BitsetInlined operator&(const BitsetInlined &lhs,
const BitsetInlined &rhs) noexcept {
BitsetInlined result = lhs;
if (result._storage.template is<fixed_bitset>() &&
rhs._storage.template is<fixed_bitset>()) {
// Both are fixed, use the fixed implementation
*result._storage.template ptr<fixed_bitset>() &=
*rhs._storage.template ptr<fixed_bitset>();
} else {
// At least one is dynamic, handle bit by bit
fl::u32 min_size =
result.size() < rhs.size() ? result.size() : rhs.size();
for (fl::u32 i = 0; i < min_size; ++i) {
result.set(i, result.test(i) && rhs.test(i));
}
// Clear any bits beyond the size of rhs
for (fl::u32 i = min_size; i < result.size(); ++i) {
result.reset(i);
}
}
return result;
}
friend BitsetInlined operator|(const BitsetInlined &lhs,
const BitsetInlined &rhs) noexcept {
BitsetInlined result = lhs;
if (result._storage.template is<fixed_bitset>() &&
rhs._storage.template is<fixed_bitset>()) {
// Both are fixed, use the fixed implementation
*result._storage.template ptr<fixed_bitset>() |=
*rhs._storage.template ptr<fixed_bitset>();
} else {
// At least one is dynamic, handle bit by bit
fl::u32 max_size =
result.size() > rhs.size() ? result.size() : rhs.size();
// Resize if needed
if (result.size() < max_size) {
result.resize(max_size);
}
// Set bits from rhs
for (fl::u32 i = 0; i < rhs.size(); ++i) {
if (rhs.test(i)) {
result.set(i);
}
}
}
return result;
}
friend BitsetInlined operator^(const BitsetInlined &lhs,
const BitsetInlined &rhs) noexcept {
BitsetInlined result = lhs;
if (result._storage.template is<fixed_bitset>() &&
rhs._storage.template is<fixed_bitset>()) {
// Both are fixed, use the fixed implementation
*result._storage.template ptr<fixed_bitset>() ^=
*rhs._storage.template ptr<fixed_bitset>();
} else {
// At least one is dynamic, handle bit by bit
fl::u32 max_size =
result.size() > rhs.size() ? result.size() : rhs.size();
// Resize if needed
if (result.size() < max_size) {
result.resize(max_size);
}
// XOR bits from rhs
for (fl::u32 i = 0; i < rhs.size(); ++i) {
result.set(i, result.test(i) != rhs.test(i));
}
}
return result;
}
};
} // namespace fl
FL_DISABLE_WARNING_POP

View File

@@ -0,0 +1,451 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
#include <string.h> // for memcpy
#include "fl/math_macros.h"
#include "fl/memfill.h"
#include "fl/compiler_control.h"
namespace fl {
class string;
namespace detail {
void to_string(const fl::u16 *bit_data, fl::u32 bit_count, string* dst);
}
/// A dynamic bitset implementation that can be resized at runtime
class bitset_dynamic {
private:
static constexpr fl::u32 bits_per_block = 8 * sizeof(fl::u16);
using block_type = fl::u16;
block_type *_blocks = nullptr;
fl::u32 _block_count = 0;
fl::u32 _size = 0;
// Helper to calculate block count from bit count
static fl::u32 calc_block_count(fl::u32 bit_count) {
return (bit_count + bits_per_block - 1) / bits_per_block;
}
public:
// Default constructor
bitset_dynamic() = default;
// Constructor with initial size
explicit bitset_dynamic(fl::u32 size) { resize(size); }
// Copy constructor
bitset_dynamic(const bitset_dynamic &other) {
if (other._size > 0) {
resize(other._size);
memcpy(_blocks, other._blocks, _block_count * sizeof(block_type));
}
}
// Move constructor
bitset_dynamic(bitset_dynamic &&other) noexcept
: _blocks(other._blocks), _block_count(other._block_count),
_size(other._size) {
other._blocks = nullptr;
other._block_count = 0;
other._size = 0;
}
// Copy assignment
bitset_dynamic &operator=(const bitset_dynamic &other) {
if (this != &other) {
if (other._size > 0) {
resize(other._size);
memcpy(_blocks, other._blocks,
_block_count * sizeof(block_type));
} else {
clear();
}
}
return *this;
}
// Move assignment
bitset_dynamic &operator=(bitset_dynamic &&other) noexcept {
if (this != &other) {
delete[] _blocks;
_blocks = other._blocks;
_block_count = other._block_count;
_size = other._size;
other._blocks = nullptr;
other._block_count = 0;
other._size = 0;
}
return *this;
}
// Destructor
~bitset_dynamic() { delete[] _blocks; }
// Assign n bits to the value specified
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
void assign(fl::u32 n, bool value) {
if (n > _size) {
resize(n);
}
if (value) {
// Set all bits to 1
if (_blocks && _block_count > 0) {
for (fl::u32 i = 0; i < _block_count; ++i) {
_blocks[i] = static_cast<block_type>(~block_type(0));
}
// Clear any bits beyond the actual size
if (_size % bits_per_block != 0) {
fl::u32 last_block_idx = (_size - 1) / bits_per_block;
fl::u32 last_bit_pos = (_size - 1) % bits_per_block;
block_type mask = static_cast<block_type>((static_cast<block_type>(1) << (last_bit_pos + 1)) - 1);
_blocks[last_block_idx] &= mask;
}
}
} else {
// Set all bits to 0
reset();
}
}
FL_DISABLE_WARNING_POP
// Resize the bitset
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
void resize(fl::u32 new_size) {
if (new_size == _size)
return;
fl::u32 new_block_count = (new_size + bits_per_block - 1) / bits_per_block;
if (new_block_count != _block_count) {
block_type *new_blocks = new block_type[new_block_count];
fl::memfill(new_blocks, 0, new_block_count * sizeof(block_type));
if (_blocks) {
fl::u32 copy_blocks = MIN(_block_count, new_block_count);
memcpy(new_blocks, _blocks, copy_blocks * sizeof(block_type));
}
delete[] _blocks;
_blocks = new_blocks;
_block_count = new_block_count;
}
_size = new_size;
// Clear any bits beyond the new size
if (_blocks && _block_count > 0 && _size % bits_per_block != 0) {
fl::u32 last_block_idx = (_size - 1) / bits_per_block;
fl::u32 last_bit_pos = (_size - 1) % bits_per_block;
block_type mask =
static_cast<block_type>((static_cast<block_type>(1) << (last_bit_pos + 1)) - 1);
_blocks[last_block_idx] &= mask;
}
}
FL_DISABLE_WARNING_POP
// Clear the bitset (reset to empty)
void clear() {
delete[] _blocks;
_blocks = nullptr;
_block_count = 0;
_size = 0;
}
// Reset all bits to 0
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
void reset() noexcept {
if (_blocks && _block_count > 0) {
fl::memfill(_blocks, 0, _block_count * sizeof(block_type));
}
}
FL_DISABLE_WARNING_POP
// Reset a specific bit to 0
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
void reset(fl::u32 pos) noexcept {
if (_blocks && pos < _size) {
const fl::u32 idx = pos / bits_per_block;
const fl::u32 off = pos % bits_per_block;
_blocks[idx] &= ~(static_cast<block_type>(1) << off);
}
}
FL_DISABLE_WARNING_POP
// Set a specific bit to 1
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
void set(fl::u32 pos) noexcept {
if (_blocks && pos < _size) {
const fl::u32 idx = pos / bits_per_block;
const fl::u32 off = pos % bits_per_block;
_blocks[idx] |= (static_cast<block_type>(1) << off);
}
}
FL_DISABLE_WARNING_POP
// Set a specific bit to a given value
void set(fl::u32 pos, bool value) noexcept {
if (value) {
set(pos);
} else {
reset(pos);
}
}
// Flip a specific bit
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
void flip(fl::u32 pos) noexcept {
if (_blocks && pos < _size) {
const fl::u32 idx = pos / bits_per_block;
const fl::u32 off = pos % bits_per_block;
_blocks[idx] ^= (static_cast<block_type>(1) << off);
}
}
FL_DISABLE_WARNING_POP
// Flip all bits
void flip() noexcept {
if (!_blocks) return;
for (fl::u32 i = 0; i < _block_count; ++i) {
_blocks[i] = ~_blocks[i];
}
// Clear any bits beyond size
if (_block_count > 0 && _size % bits_per_block != 0) {
fl::u32 last_block_idx = (_size - 1) / bits_per_block;
fl::u32 last_bit_pos = (_size - 1) % bits_per_block;
block_type mask =
static_cast<block_type>((static_cast<block_type>(1) << (last_bit_pos + 1)) - 1);
_blocks[last_block_idx] &= mask;
}
}
// Test if a bit is set
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
bool test(fl::u32 pos) const noexcept {
if (_blocks && pos < _size) {
const fl::u32 idx = pos / bits_per_block;
const fl::u32 off = pos % bits_per_block;
return (_blocks[idx] >> off) & 1;
}
return false;
}
FL_DISABLE_WARNING_POP
// Count the number of set bits
fl::u32 count() const noexcept {
if (!_blocks) return 0;
fl::u32 result = 0;
for (fl::u32 i = 0; i < _block_count; ++i) {
result += static_cast<fl::u32>(__builtin_popcount(_blocks[i]));
}
return result;
}
// Check if any bit is set
bool any() const noexcept {
if (!_blocks) return false;
for (fl::u32 i = 0; i < _block_count; ++i) {
if (_blocks[i] != 0)
return true;
}
return false;
}
// Check if no bit is set
bool none() const noexcept { return !any(); }
// Check if all bits are set
bool all() const noexcept {
if (_size == 0)
return true;
if (!_blocks) return false;
for (fl::u32 i = 0; i < _block_count - 1; ++i) {
if (_blocks[i] != static_cast<block_type>(~block_type(0)))
return false;
}
// Check last block with mask for valid bits
if (_block_count > 0) {
fl::u32 last_bit_pos = (_size - 1) % bits_per_block;
block_type mask =
static_cast<block_type>((static_cast<block_type>(1) << (last_bit_pos + 1)) - 1);
return (_blocks[_block_count - 1] & mask) == mask;
}
return true;
}
// Get the size of the bitset
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
fl::u32 size() const noexcept {
// Note: _size is a member variable, not a pointer, so this should be safe
// but we add this comment to clarify for static analysis
return _size;
}
FL_DISABLE_WARNING_POP
// Convert bitset to string representation
void to_string(string* dst) const;
// Access operator
bool operator[](fl::u32 pos) const noexcept { return test(pos); }
/// Finds the first bit that matches the test value.
/// Returns the index of the first matching bit, or -1 if none found.
/// @param test_value The value to search for (true or false)
/// @param offset Starting position to search from (default: 0)
fl::i32 find_first(bool test_value, fl::u32 offset = 0) const noexcept {
// If offset is beyond our size, no match possible
if (offset >= _size) {
return -1;
}
// Calculate which block to start from
fl::u32 start_block = offset / bits_per_block;
fl::u32 start_bit = offset % bits_per_block;
for (fl::u32 block_idx = start_block; block_idx < _block_count; ++block_idx) {
block_type current_block = _blocks[block_idx];
// For the last block, we need to mask out unused bits
if (block_idx == _block_count - 1 && _size % bits_per_block != 0) {
const fl::u32 valid_bits = _size % bits_per_block;
block_type mask = (valid_bits == bits_per_block)
? static_cast<block_type>(~block_type(0))
: static_cast<block_type>(((block_type(1) << valid_bits) - 1));
current_block &= mask;
}
// If looking for false bits, invert the block
if (!test_value) {
current_block = ~current_block;
}
// For the first block, mask out bits before the offset
if (block_idx == start_block && start_bit > 0) {
current_block &= ~static_cast<block_type>(((block_type(1) << start_bit) - 1));
}
// If there are any matching bits in this block
if (current_block != 0) {
// Find the first set bit
fl::u32 bit_pos = static_cast<fl::u32>(__builtin_ctz(current_block));
fl::u32 absolute_pos = block_idx * bits_per_block + bit_pos;
// Make sure we haven't gone past the end of the bitset
if (absolute_pos < _size) {
return static_cast<fl::i32>(absolute_pos);
}
}
}
return -1; // No matching bit found
}
// Bitwise AND operator
bitset_dynamic operator&(const bitset_dynamic &other) const {
bitset_dynamic result(_size);
if (!_blocks || !other._blocks || !result._blocks) {
return result;
}
fl::u32 min_blocks = MIN(_block_count, other._block_count);
for (fl::u32 i = 0; i < min_blocks; ++i) {
result._blocks[i] = _blocks[i] & other._blocks[i];
}
return result;
}
// Bitwise OR operator
bitset_dynamic operator|(const bitset_dynamic &other) const {
bitset_dynamic result(_size);
if (!_blocks || !other._blocks || !result._blocks) {
return result;
}
fl::u32 min_blocks = MIN(_block_count, other._block_count);
for (fl::u32 i = 0; i < min_blocks; ++i) {
result._blocks[i] = _blocks[i] | other._blocks[i];
}
// Copy remaining blocks from the larger bitset
if (_block_count > min_blocks) {
memcpy(result._blocks + min_blocks, _blocks + min_blocks,
(_block_count - min_blocks) * sizeof(block_type));
}
return result;
}
// Bitwise XOR operator
bitset_dynamic operator^(const bitset_dynamic &other) const {
bitset_dynamic result(_size);
if (!_blocks || !other._blocks || !result._blocks) {
return result;
}
fl::u32 min_blocks = MIN(_block_count, other._block_count);
for (fl::u32 i = 0; i < min_blocks; ++i) {
result._blocks[i] = _blocks[i] ^ other._blocks[i];
}
// Copy remaining blocks from the larger bitset
if (_block_count > min_blocks) {
memcpy(result._blocks + min_blocks, _blocks + min_blocks,
(_block_count - min_blocks) * sizeof(block_type));
}
return result;
}
// Bitwise NOT operator
bitset_dynamic operator~() const {
bitset_dynamic result(_size);
if (!_blocks || !result._blocks) {
return result;
}
for (fl::u32 i = 0; i < _block_count; ++i) {
result._blocks[i] = ~_blocks[i];
}
// Clear any bits beyond size
if (_block_count > 0 && _size % bits_per_block != 0) {
fl::u32 last_block_idx = (_size - 1) / bits_per_block;
fl::u32 last_bit_pos = (_size - 1) % bits_per_block;
block_type mask =
static_cast<block_type>((static_cast<block_type>(1) << (last_bit_pos + 1)) - 1);
result._blocks[last_block_idx] &= mask;
}
return result;
}
};
} // namespace fl

View File

@@ -0,0 +1,134 @@
#include "fl/stdint.h"
#define FASTLED_INTERNAL
#include "FastLED.h"
#include "crgb.h"
#include "fl/blur.h"
#include "fl/colorutils_misc.h"
#include "fl/compiler_control.h"
#include "fl/deprecated.h"
#include "fl/unused.h"
#include "fl/xymap.h"
#include "lib8tion/scale8.h"
#include "fl/int.h"
namespace fl {
// Legacy XY function. This is a weak symbol that can be overridden by the user.
fl::u16 XY(fl::u8 x, fl::u8 y) FL_LINK_WEAK;
FL_LINK_WEAK fl::u16 XY(fl::u8 x, fl::u8 y) {
FASTLED_UNUSED(x);
FASTLED_UNUSED(y);
FASTLED_ASSERT(false, "the user didn't provide an XY function");
return 0;
}
// fl::u16 XY(fl::u8 x, fl::u8 y) {
// return 0;
// }
// make this a weak symbol
namespace {
fl::u16 xy_legacy_wrapper(fl::u16 x, fl::u16 y, fl::u16 width,
fl::u16 height) {
FASTLED_UNUSED(width);
FASTLED_UNUSED(height);
return XY(x, y);
}
} // namespace
// blur1d: one-dimensional blur filter. Spreads light to 2 line neighbors.
// blur2d: two-dimensional blur filter. Spreads light to 8 XY neighbors.
//
// 0 = no spread at all
// 64 = moderate spreading
// 172 = maximum smooth, even spreading
//
// 173..255 = wider spreading, but increasing flicker
//
// Total light is NOT entirely conserved, so many repeated
// calls to 'blur' will also result in the light fading,
// eventually all the way to black; this is by design so that
// it can be used to (slowly) clear the LEDs to black.
void blur1d(CRGB *leds, fl::u16 numLeds, fract8 blur_amount) {
fl::u8 keep = 255 - blur_amount;
fl::u8 seep = blur_amount >> 1;
CRGB carryover = CRGB::Black;
for (fl::u16 i = 0; i < numLeds; ++i) {
CRGB cur = leds[i];
CRGB part = cur;
part.nscale8(seep);
cur.nscale8(keep);
cur += carryover;
if (i)
leds[i - 1] += part;
leds[i] = cur;
carryover = part;
}
}
void blur2d(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount,
const XYMap &xymap) {
blurRows(leds, width, height, blur_amount, xymap);
blurColumns(leds, width, height, blur_amount, xymap);
}
void blur2d(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount) {
XYMap xy =
XYMap::constructWithUserFunction(width, height, xy_legacy_wrapper);
blur2d(leds, width, height, blur_amount, xy);
}
void blurRows(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount,
const XYMap &xyMap) {
/* for( fl::u8 row = 0; row < height; row++) {
CRGB* rowbase = leds + (row * width);
blur1d( rowbase, width, blur_amount);
}
*/
// blur rows same as columns, for irregular matrix
fl::u8 keep = 255 - blur_amount;
fl::u8 seep = blur_amount >> 1;
for (fl::u8 row = 0; row < height; row++) {
CRGB carryover = CRGB::Black;
for (fl::u8 i = 0; i < width; i++) {
CRGB cur = leds[xyMap.mapToIndex(i, row)];
CRGB part = cur;
part.nscale8(seep);
cur.nscale8(keep);
cur += carryover;
if (i)
leds[xyMap.mapToIndex(i - 1, row)] += part;
leds[xyMap.mapToIndex(i, row)] = cur;
carryover = part;
}
}
}
// blurColumns: perform a blur1d on each column of a rectangular matrix
void blurColumns(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount,
const XYMap &xyMap) {
// blur columns
fl::u8 keep = 255 - blur_amount;
fl::u8 seep = blur_amount >> 1;
for (fl::u8 col = 0; col < width; ++col) {
CRGB carryover = CRGB::Black;
for (fl::u8 i = 0; i < height; ++i) {
CRGB cur = leds[xyMap.mapToIndex(col, i)];
CRGB part = cur;
part.nscale8(seep);
cur.nscale8(keep);
cur += carryover;
if (i)
leds[xyMap.mapToIndex(col, i - 1)] += part;
leds[xyMap.mapToIndex(col, i)] = cur;
carryover = part;
}
}
}
} // namespace fl

View File

@@ -0,0 +1,71 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
#include "crgb.h"
#include "fl/deprecated.h"
namespace fl {
/// @defgroup ColorBlurs Color Blurring Functions
/// Functions for blurring colors
/// @{
/// One-dimensional blur filter.
/// Spreads light to 2 line neighbors.
/// * 0 = no spread at all
/// * 64 = moderate spreading
/// * 172 = maximum smooth, even spreading
/// * 173..255 = wider spreading, but increasing flicker
///
/// Total light is NOT entirely conserved, so many repeated
/// calls to 'blur' will also result in the light fading,
/// eventually all the way to black; this is by design so that
/// it can be used to (slowly) clear the LEDs to black.
/// @param leds a pointer to the LED array to blur
/// @param numLeds the number of LEDs to blur
/// @param blur_amount the amount of blur to apply
void blur1d(CRGB *leds, u16 numLeds, fract8 blur_amount);
/// Two-dimensional blur filter.
/// Spreads light to 8 XY neighbors.
/// * 0 = no spread at all
/// * 64 = moderate spreading
/// * 172 = maximum smooth, even spreading
/// * 173..255 = wider spreading, but increasing flicker
///
/// Total light is NOT entirely conserved, so many repeated
/// calls to 'blur' will also result in the light fading,
/// eventually all the way to black; this is by design so that
/// it can be used to (slowly) clear the LEDs to black.
/// @param leds a pointer to the LED array to blur
/// @param width the width of the matrix
/// @param height the height of the matrix
/// @param blur_amount the amount of blur to apply
void blur2d(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount,
const fl::XYMap &xymap);
/// Legacy version of blur2d, which does not require an XYMap but instead
/// implicitly binds to XY() function. If you are hitting a linker error here,
/// then use blur2d(..., const fl::XYMap& xymap) instead.
void blur2d(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount)
FASTLED_DEPRECATED("Use blur2d(..., const fl::XYMap& xymap) instead");
/// Perform a blur1d() on every row of a rectangular matrix
/// @see blur1d()
/// @param leds a pointer to the LED array to blur
/// @param width the width of the matrix
/// @param height the height of the matrix
/// @param blur_amount the amount of blur to apply
void blurRows(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount,
const fl::XYMap &xymap);
/// Perform a blur1d() on every column of a rectangular matrix
/// @copydetails blurRows()
void blurColumns(CRGB *leds, fl::u8 width, fl::u8 height, fract8 blur_amount,
const fl::XYMap &xymap);
/// @} ColorBlurs
} // namespace fl

View File

@@ -0,0 +1,29 @@
#pragma once
#include "fl/stdint.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/int.h"
#include "crgb.h"
namespace fl {
FASTLED_SMART_PTR(ByteStream);
// An abstract class that represents a stream of bytes.
class ByteStream {
public:
virtual ~ByteStream() {}
virtual bool available(fl::size) const = 0;
virtual fl::size read(fl::u8 *dst, fl::size bytesToRead) = 0;
virtual const char *path() const = 0;
virtual void close() {} // default is do nothing on close.
// convenience functions
virtual fl::size readCRGB(CRGB *dst, fl::size n) {
return read((fl::u8 *)dst, n * 3) / 3;
}
};
} // namespace fl

View File

@@ -0,0 +1,72 @@
#include <string.h>
#include "fl/int.h"
#include "fl/bytestreammemory.h"
#include "fl/math_macros.h"
#include "fl/namespace.h"
#include "fl/warn.h"
namespace fl {
ByteStreamMemory::ByteStreamMemory(fl::u32 size_buffer)
: mReadBuffer(size_buffer) {}
ByteStreamMemory::~ByteStreamMemory() = default;
bool ByteStreamMemory::available(fl::size n) const {
return mReadBuffer.size() >= n;
}
fl::size ByteStreamMemory::read(fl::u8 *dst, fl::size bytesToRead) {
if (!available(bytesToRead) || dst == nullptr) {
FASTLED_WARN("ByteStreamMemory::read: !available(bytesToRead): "
<< bytesToRead
<< " mReadBuffer.size(): " << mReadBuffer.size());
return 0;
}
fl::size actualBytesToRead = MIN(bytesToRead, mReadBuffer.size());
fl::size bytesRead = 0;
while (bytesRead < actualBytesToRead) {
fl::u8 &b = dst[bytesRead];
mReadBuffer.pop_front(&b);
bytesRead++;
}
if (bytesRead == 0) {
FASTLED_WARN("ByteStreamMemory::read: bytesRead == 0");
}
return bytesRead;
}
fl::size ByteStreamMemory::write(const fl::u8 *src, fl::size n) {
if (src == nullptr || mReadBuffer.capacity() == 0) {
FASTLED_WARN_IF(src == nullptr,
"ByteStreamMemory::write: src == nullptr");
FASTLED_WARN_IF(mReadBuffer.capacity() == 0,
"ByteStreamMemory::write: mReadBuffer.capacity() == 0");
return 0;
}
fl::size written = 0;
for (fl::size i = 0; i < n; ++i) {
if (mReadBuffer.full()) {
FASTLED_WARN("ByteStreamMemory::write: mReadBuffer.full(): "
<< mReadBuffer.size());
break;
}
mReadBuffer.push_back(src[i]);
++written;
}
return written;
}
fl::size ByteStreamMemory::writeCRGB(const CRGB *src, fl::size n) {
fl::size bytes_written = write(reinterpret_cast<const fl::u8 *>(src), n * 3);
fl::size pixels_written = bytes_written / 3;
return pixels_written;
}
} // namespace fl

View File

@@ -0,0 +1,31 @@
#pragma once
#include "fl/stdint.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/bytestream.h"
#include "fl/circular_buffer.h"
#include "fl/int.h"
namespace fl {
FASTLED_SMART_PTR(ByteStreamMemory);
class ByteStreamMemory : public ByteStream {
public:
ByteStreamMemory(fl::u32 size_buffer);
~ByteStreamMemory() override;
bool available(fl::size n) const override;
fl::size read(fl::u8 *dst, fl::size bytesToRead) override;
void clear() { mReadBuffer.clear(); }
const char *path() const override { return "ByteStreamMemory"; }
fl::size write(const fl::u8 *src, fl::size n);
fl::size writeCRGB(const CRGB *src, fl::size n);
private:
CircularBuffer<fl::u8> mReadBuffer;
};
} // namespace fl

View File

@@ -0,0 +1,143 @@
#pragma once
#include "fl/math_macros.h"
#include "fl/namespace.h"
#include "fl/scoped_array.h"
#include "fl/stdint.h" // For standard integer types
namespace fl {
// TODO:
// ERROR bit to indicate over flow.
// Static version with compile-time capacity
template <typename T, fl::size N>
class StaticCircularBuffer {
public:
StaticCircularBuffer() : mHead(0), mTail(0) {}
void push(const T &value) {
if (full()) {
mTail = (mTail + 1) % (N + 1); // Overwrite the oldest element
}
mBuffer[mHead] = value;
mHead = (mHead + 1) % (N + 1);
}
bool pop(T &value) {
if (empty()) {
return false;
}
value = mBuffer[mTail];
mTail = (mTail + 1) % (N + 1);
return true;
}
fl::size size() const { return (mHead + N + 1 - mTail) % (N + 1); }
constexpr fl::size capacity() const { return N; }
bool empty() const { return mHead == mTail; }
bool full() const { return ((mHead + 1) % (N + 1)) == mTail; }
void clear() { mHead = mTail = 0; }
private:
T mBuffer[N + 1]; // Extra space for distinguishing full/empty
fl::size mHead;
fl::size mTail;
};
// Dynamic version with runtime capacity (existing implementation)
template <typename T> class DynamicCircularBuffer {
public:
DynamicCircularBuffer(fl::size capacity)
: mCapacity(capacity + 1), mHead(0),
mTail(0) { // Extra space for distinguishing full/empty
mBuffer.reset(new T[mCapacity]);
}
DynamicCircularBuffer(const DynamicCircularBuffer &) = delete;
DynamicCircularBuffer &operator=(const DynamicCircularBuffer &) = delete;
bool push_back(const T &value) {
if (full()) {
mTail = increment(mTail); // Overwrite the oldest element
}
mBuffer[mHead] = value;
mHead = increment(mHead);
return true;
}
bool pop_front(T *dst = nullptr) {
if (empty()) {
return false;
}
if (dst) {
*dst = mBuffer[mTail];
}
mTail = increment(mTail);
return true;
}
bool push_front(const T &value) {
if (full()) {
mHead = decrement(mHead); // Overwrite the oldest element
}
mTail = decrement(mTail);
mBuffer[mTail] = value;
return true;
}
bool pop_back(T *dst = nullptr) {
if (empty()) {
return false;
}
mHead = decrement(mHead);
if (dst) {
*dst = mBuffer[mHead];
}
return true;
}
T &front() { return mBuffer[mTail]; }
const T &front() const { return mBuffer[mTail]; }
T &back() { return mBuffer[(mHead + mCapacity - 1) % mCapacity]; }
const T &back() const {
return mBuffer[(mHead + mCapacity - 1) % mCapacity];
}
T &operator[](fl::size index) { return mBuffer[(mTail + index) % mCapacity]; }
const T &operator[](fl::size index) const {
return mBuffer[(mTail + index) % mCapacity];
}
fl::size size() const { return (mHead + mCapacity - mTail) % mCapacity; }
fl::size capacity() const { return mCapacity - 1; }
bool empty() const { return mHead == mTail; }
bool full() const { return increment(mHead) == mTail; }
void clear() { mHead = mTail = 0; }
private:
fl::size increment(fl::size index) const { return (index + 1) % mCapacity; }
fl::size decrement(fl::size index) const {
return (index + mCapacity - 1) % mCapacity;
}
fl::scoped_array<T> mBuffer;
fl::size mCapacity;
fl::size mHead;
fl::size mTail;
};
// For backward compatibility, keep the old name for the dynamic version
template <typename T>
using CircularBuffer = DynamicCircularBuffer<T>;
} // namespace fl

View File

@@ -0,0 +1,20 @@
#pragma once
#include "fl/stdint.h"
#include "fl/force_inline.h"
namespace fl {
template <typename T> FASTLED_FORCE_INLINE T clamp(T value, T min, T max) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
} // namespace fl

View File

@@ -0,0 +1,39 @@
#pragma once
#include "fl/leds.h"
#include "fl/stdint.h"
namespace fl {
template<typename T>
class Grid;
// Memory safe clear function for CRGB arrays.
template <int N> inline void clear(CRGB (&arr)[N]) {
for (int i = 0; i < N; ++i) {
arr[i] = CRGB::Black;
}
}
inline void clear(Leds &leds) { leds.fill(CRGB::Black); }
template<fl::size W, fl::size H>
inline void clear(LedsXY<W, H> &leds) {
leds.fill(CRGB::Black);
}
template<typename T>
inline void clear(Grid<T> &grid) {
grid.clear();
}
// Default, when you don't know what do then call clear.
template<typename Container>
inline void clear(Container &container) {
container.clear();
}
} // namespace fl

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
// TODO: Figure out how to namespace these.
typedef fl::u32 TProgmemRGBPalette16[16]; ///< CRGBPalette16 entries stored in
///< PROGMEM memory
typedef fl::u32 TProgmemHSVPalette16[16]; ///< CHSVPalette16 entries stored in
///< PROGMEM memory
/// Alias for TProgmemRGBPalette16
#define TProgmemPalette16 TProgmemRGBPalette16
typedef fl::u32 TProgmemRGBPalette32[32]; ///< CRGBPalette32 entries stored in
///< PROGMEM memory
typedef fl::u32 TProgmemHSVPalette32[32]; ///< CHSVPalette32 entries stored in
///< PROGMEM memory
/// Alias for TProgmemRGBPalette32
#define TProgmemPalette32 TProgmemRGBPalette32
/// Byte of an RGB gradient, stored in PROGMEM memory
typedef const fl::u8 TProgmemRGBGradientPalette_byte;
/// Pointer to bytes of an RGB gradient, stored in PROGMEM memory
/// @see DEFINE_GRADIENT_PALETTE
/// @see DECLARE_GRADIENT_PALETTE
typedef const TProgmemRGBGradientPalette_byte *TProgmemRGBGradientPalette_bytes;
/// Alias of ::TProgmemRGBGradientPalette_bytes
typedef TProgmemRGBGradientPalette_bytes TProgmemRGBGradientPaletteRef;
namespace fl {
/// Hue direction for calculating fill gradients.
/// Since "hue" is a value around a color wheel, there are always two directions
/// to sweep from one hue to another.
typedef enum {
FORWARD_HUES, ///< Hue always goes clockwise around the color wheel
BACKWARD_HUES, ///< Hue always goes counter-clockwise around the color wheel
SHORTEST_HUES, ///< Hue goes whichever way is shortest
LONGEST_HUES ///< Hue goes whichever way is longest
} TGradientDirectionCode;
} // namespace fl

View File

@@ -0,0 +1,9 @@
#pragma once
#include "fl/utility.h"
namespace fl {
// DefaultLess is now an alias for less<T>, defined in utility.h
} // namespace fl

View File

@@ -0,0 +1,146 @@
#pragma once
// Stringify helper for pragma arguments
#define FL_STRINGIFY2(x) #x
#define FL_STRINGIFY(x) FL_STRINGIFY2(x)
// BEGIN BASE MACROS
#if defined(__clang__)
#define FL_DISABLE_WARNING_PUSH _Pragma("clang diagnostic push")
#define FL_DISABLE_WARNING_POP _Pragma("clang diagnostic pop")
// Usage: FL_DISABLE_WARNING(float-equal)
#define FL_DISABLE_WARNING(warning) _Pragma(FL_STRINGIFY(clang diagnostic ignored "-W" #warning))
#elif defined(__GNUC__) && (__GNUC__*100 + __GNUC_MINOR__) >= 406
#define FL_DISABLE_WARNING_PUSH _Pragma("GCC diagnostic push")
#define FL_DISABLE_WARNING_POP _Pragma("GCC diagnostic pop")
// Usage: FL_DISABLE_WARNING(float-equal)
#define FL_DISABLE_WARNING(warning) _Pragma(FL_STRINGIFY(GCC diagnostic ignored "-W" #warning))
#else
#define FL_DISABLE_WARNING_PUSH
#define FL_DISABLE_WARNING_POP
#define FL_DISABLE_WARNING(warning)
#endif
// END BASE MACROS
// WARNING SPECIFIC MACROS THAT MAY NOT BE UNIVERSAL.
#if defined(__clang__)
#define FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS \
FL_DISABLE_WARNING(global-constructors)
#define FL_DISABLE_WARNING_SELF_ASSIGN_OVERLOADED \
FL_DISABLE_WARNING(self-assign-overloaded)
// Clang doesn't have format-truncation warning, use no-op
#define FL_DISABLE_FORMAT_TRUNCATION
#define FL_DISABLE_WARNING_NULL_DEREFERENCE FL_DISABLE_WARNING(null-dereference)
#define FL_DISABLE_WARNING_IMPLICIT_FALLTHROUGH
#define FL_DISABLE_WARNING_UNUSED_PARAMETER
#define FL_DISABLE_WARNING_RETURN_TYPE
#define FL_DISABLE_WARNING_IMPLICIT_INT_CONVERSION FL_DISABLE_WARNING(implicit-int-conversion)
#define FL_DISABLE_WARNING_FLOAT_CONVERSION FL_DISABLE_WARNING(float-conversion)
#define FL_DISABLE_WARNING_SIGN_CONVERSION FL_DISABLE_WARNING(sign-conversion)
#define FL_DISABLE_WARNING_SHORTEN_64_TO_32 FL_DISABLE_WARNING(shorten-64-to-32)
#elif defined(__GNUC__) && (__GNUC__*100 + __GNUC_MINOR__) >= 406
// GCC doesn't have global-constructors warning, use no-op
#define FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS
// GCC doesn't have self-assign-overloaded warning, use no-op
#define FL_DISABLE_WARNING_SELF_ASSIGN_OVERLOADED
// GCC has format-truncation warning
#define FL_DISABLE_FORMAT_TRUNCATION \
FL_DISABLE_WARNING(format-truncation)
#define FL_DISABLE_WARNING_NULL_DEREFERENCE
#define FL_DISABLE_WARNING_UNUSED_PARAMETER \
FL_DISABLE_WARNING(unused-parameter)
#define FL_DISABLE_WARNING_RETURN_TYPE \
FL_DISABLE_WARNING(return-type)
// implicit-fallthrough warning requires GCC >= 7.0
#if (__GNUC__*100 + __GNUC_MINOR__) >= 700
#define FL_DISABLE_WARNING_IMPLICIT_FALLTHROUGH FL_DISABLE_WARNING(implicit-fallthrough)
#else
#define FL_DISABLE_WARNING_IMPLICIT_FALLTHROUGH
#endif
// GCC doesn't support these conversion warnings on older versions
#define FL_DISABLE_WARNING_FLOAT_CONVERSION
#define FL_DISABLE_WARNING_SIGN_CONVERSION
#define FL_DISABLE_WARNING_IMPLICIT_INT_CONVERSION
// GCC doesn't have shorten-64-to-32 warning, use no-op
#define FL_DISABLE_WARNING_SHORTEN_64_TO_32
#else
#define FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS
#define FL_DISABLE_WARNING_SELF_ASSIGN_OVERLOADED
#define FL_DISABLE_FORMAT_TRUNCATION
#define FL_DISABLE_WARNING_NULL_DEREFERENCE
#define FL_DISABLE_WARNING_UNUSED_PARAMETER
#define FL_DISABLE_WARNING_RETURN_TYPE
#define FL_DISABLE_WARNING_IMPLICIT_INT_CONVERSION
#define FL_DISABLE_WARNING_FLOAT_CONVERSION
#define FL_DISABLE_WARNING_SIGN_CONVERSION
#define FL_DISABLE_WARNING_SHORTEN_64_TO_32
#endif
// END WARNING SPECIFIC MACROS THAT MAY NOT BE UNIVERSAL.
// Fast math optimization controls with additional aggressive flags
#if defined(__clang__)
#define FL_FAST_MATH_BEGIN \
_Pragma("clang diagnostic push") \
_Pragma("STDC FP_CONTRACT ON")
#define FL_FAST_MATH_END _Pragma("clang diagnostic pop")
#elif defined(__GNUC__)
#define FL_FAST_MATH_BEGIN \
_Pragma("GCC push_options") \
_Pragma("GCC optimize (\"fast-math\")") \
_Pragma("GCC optimize (\"tree-vectorize\")") \
_Pragma("GCC optimize (\"unroll-loops\")")
#define FL_FAST_MATH_END _Pragma("GCC pop_options")
#elif defined(_MSC_VER)
#define FL_FAST_MATH_BEGIN __pragma(float_control(precise, off))
#define FL_FAST_MATH_END __pragma(float_control(precise, on))
#else
#define FL_FAST_MATH_BEGIN /* nothing */
#define FL_FAST_MATH_END /* nothing */
#endif
// Optimization Level O3
#if defined(__clang__)
#define FL_OPTIMIZATION_LEVEL_O3_BEGIN \
_Pragma("clang diagnostic push")
#define FL_OPTIMIZATION_LEVEL_O3_END _Pragma("clang diagnostic pop")
#elif defined(__GNUC__)
#define FL_OPTIMIZATION_LEVEL_O3_BEGIN \
_Pragma("GCC push_options") \
_Pragma("GCC optimize (\"O3\")")
#define FL_OPTIMIZATION_LEVEL_O3_END _Pragma("GCC pop_options")
#else
#define FL_OPTIMIZATION_LEVEL_O3_BEGIN /* nothing */
#define FL_OPTIMIZATION_LEVEL_O3_END /* nothing */
#endif
// Optimization Level O0 (Debug/No optimization)
#if defined(__clang__)
#define FL_OPTIMIZATION_LEVEL_O0_BEGIN \
_Pragma("clang diagnostic push")
#define FL_OPTIMIZATION_LEVEL_O0_END _Pragma("clang diagnostic pop")
#elif defined(__GNUC__)
#define FL_OPTIMIZATION_LEVEL_O0_BEGIN \
_Pragma("GCC push_options") \
_Pragma("GCC optimize (\"O0\")")
#define FL_OPTIMIZATION_LEVEL_O0_END _Pragma("GCC pop_options")
#else
#define FL_OPTIMIZATION_LEVEL_O0_BEGIN /* nothing */
#define FL_OPTIMIZATION_LEVEL_O0_END /* nothing */
#endif
#ifndef FL_LINK_WEAK
#define FL_LINK_WEAK __attribute__((weak))
#endif

View File

@@ -0,0 +1,15 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
// Conversion from FastLED timings to the type found in datasheets.
inline void convert_fastled_timings_to_timedeltas(fl::u16 T1, fl::u16 T2,
fl::u16 T3, fl::u16 *T0H,
fl::u16 *T0L, fl::u16 *T1H,
fl::u16 *T1L) {
*T0H = T1;
*T0L = T2 + T3;
*T1H = T1 + T2;
*T1L = T3;
}

View File

@@ -0,0 +1,502 @@
#include "fl/corkscrew.h"
#include "fl/algorithm.h"
#include "fl/assert.h"
#include "fl/math.h"
#include "fl/splat.h"
#include "fl/warn.h"
#include "fl/tile2x2.h"
#include "fl/math_macros.h"
#include "fl/unused.h"
#include "fl/map_range.h"
#include "fl/leds.h"
#include "fl/grid.h"
#include "fl/screenmap.h"
#include "fl/memory.h"
#include "fl/int.h"
namespace fl {
namespace {
// New helper function to calculate individual LED position
vec2f calculateLedPositionExtended(fl::u16 ledIndex, fl::u16 numLeds, float totalTurns, const Gap& gapParams, fl::u16 width, fl::u16 height) {
FL_UNUSED(height);
FL_UNUSED(totalTurns);
// Check if gap feature is active AND will actually be triggered
bool gapActive = (gapParams.num_leds > 0 && gapParams.gap > 0.0f && numLeds > static_cast<fl::u16>(gapParams.num_leds));
if (!gapActive) {
// Original behavior when no gap or gap never triggers
const float ledProgress = static_cast<float>(ledIndex) / static_cast<float>(numLeds - 1);
const fl::u16 row = ledIndex / width;
const fl::u16 remainder = ledIndex % width;
const float alpha = static_cast<float>(remainder) / static_cast<float>(width);
const float width_pos = ledProgress * numLeds;
const float height_pos = static_cast<float>(row) + alpha;
return vec2f(width_pos, height_pos);
}
// Simplified gap calculation based on user expectation
// User wants: LED0=0, LED1=3, LED2=6(wraps to 0) with width=3
// This suggests they want regular spacing of width units per LED
// Simple spacing: each LED is separated by exactly width units
float width_pos = static_cast<float>(ledIndex) * static_cast<float>(width);
// For height, divide by width to get turn progress
float height_pos = width_pos / static_cast<float>(width);
return vec2f(width_pos, height_pos);
}
void calculateDimensions(float totalTurns, fl::u16 numLeds, const Gap& gapParams, fl::u16 *width, fl::u16 *height) {
FL_UNUSED(gapParams);
// Calculate optimal width and height
float ledsPerTurn = static_cast<float>(numLeds) / totalTurns;
fl::u16 calc_width = static_cast<fl::u16>(fl::ceil(ledsPerTurn));
fl::u16 height_from_turns = static_cast<fl::u16>(fl::ceil(totalTurns));
fl::u16 calc_height;
// If the grid would have more pixels than LEDs, adjust height to better match
if (calc_width * height_from_turns > numLeds) {
// Calculate height that better matches LED count
calc_height = static_cast<fl::u16>(fl::ceil(static_cast<float>(numLeds) / static_cast<float>(calc_width)));
} else {
calc_height = height_from_turns;
}
*width = calc_width;
*height = calc_height;
}
} // namespace
// New primary constructor
Corkscrew::Corkscrew(float totalTurns, fl::u16 numLeds, bool invert, const Gap& gapParams)
: mTotalTurns(totalTurns), mNumLeds(numLeds), mGapParams(gapParams), mInvert(invert) {
fl::calculateDimensions(mTotalTurns, mNumLeds, mGapParams, &mWidth, &mHeight);
mOwnsPixels = false;
}
// Constructor with external pixel buffer
Corkscrew::Corkscrew(float totalTurns, fl::span<CRGB> dstPixels, bool invert, const Gap& gapParams)
: mTotalTurns(totalTurns), mNumLeds(static_cast<fl::u16>(dstPixels.size())),
mGapParams(gapParams), mInvert(invert) {
fl::calculateDimensions(mTotalTurns, mNumLeds, mGapParams, &mWidth, &mHeight);
mPixelStorage = dstPixels;
mOwnsPixels = false; // External span
}
vec2f Corkscrew::at_no_wrap(fl::u16 i) const {
if (i >= mNumLeds) {
// Handle out-of-bounds access, possibly by returning a default value
return vec2f(0, 0);
}
// Compute position on-the-fly
vec2f position = calculateLedPositionExtended(i, mNumLeds, mTotalTurns,
mGapParams, mWidth, mHeight);
// // Apply inversion if requested
// if (mInvert) {
// fl::u16 invertedIndex = mNumLeds - 1 - i;
// position = calculateLedPositionExtended(invertedIndex, mNumLeds, mTotalTurns,
// mGapParams, mState.width, mState.height);
// }
// now wrap the x-position
//position.x = fmodf(position.x, static_cast<float>(mState.width));
return position;
}
vec2f Corkscrew::at_exact(fl::u16 i) const {
// Get the unwrapped position
vec2f position = at_no_wrap(i);
// Apply cylindrical wrapping to the x-position (like at_wrap does)
position.x = fmodf(position.x, static_cast<float>(mWidth));
return position;
}
Tile2x2_u8 Corkscrew::at_splat_extrapolate(float i) const {
if (i >= mNumLeds) {
// Handle out-of-bounds access, possibly by returning a default
// Tile2x2_u8
FASTLED_ASSERT(false, "Out of bounds access in Corkscrew at_splat: "
<< i << " size: " << mNumLeds);
return Tile2x2_u8();
}
// Use the splat function to convert the vec2f to a Tile2x2_u8
float i_floor = floorf(i);
float i_ceil = ceilf(i);
if (ALMOST_EQUAL_FLOAT(i_floor, i_ceil)) {
// If the index is the same, just return the splat of that index
vec2f position = at_no_wrap(static_cast<fl::u16>(i_floor));
return splat(position);
} else {
// Interpolate between the two points and return the splat of the result
vec2f pos1 = at_no_wrap(static_cast<fl::u16>(i_floor));
vec2f pos2 = at_no_wrap(static_cast<fl::u16>(i_ceil));
float t = i - i_floor;
vec2f interpolated_pos = map_range(t, 0.0f, 1.0f, pos1, pos2);
return splat(interpolated_pos);
}
}
fl::size Corkscrew::size() const { return mNumLeds; }
Tile2x2_u8_wrap Corkscrew::at_wrap(float i) const {
if (mCachingEnabled) {
// Use cache if enabled
initializeCache();
// Convert float index to integer for cache lookup
fl::size cache_index = static_cast<fl::size>(i);
if (cache_index < mTileCache.size()) {
return mTileCache[cache_index];
}
}
// Fall back to dynamic calculation if cache disabled or index out of bounds
return calculateTileAtWrap(i);
}
Tile2x2_u8_wrap Corkscrew::calculateTileAtWrap(float i) const {
// This is a splatted pixel, but wrapped around the cylinder.
// This is useful for rendering the corkscrew in a cylindrical way.
Tile2x2_u8 tile = at_splat_extrapolate(i);
Tile2x2_u8_wrap::Entry data[2][2];
vec2<u16> origin = tile.origin();
for (fl::u8 x = 0; x < 2; x++) {
for (fl::u8 y = 0; y < 2; y++) {
// For each pixel in the tile, map it to the cylinder so that each subcomponent
// is mapped to the correct position on the cylinder.
vec2<u16> pos = origin + vec2<u16>(x, y);
// now wrap the x-position
pos.x = fmodf(pos.x, static_cast<float>(mWidth));
data[x][y] = {pos, tile.at(x, y)};
}
}
return Tile2x2_u8_wrap(data);
}
void Corkscrew::setCachingEnabled(bool enabled) {
if (!enabled && mCachingEnabled) {
// Caching was enabled, now disabling - clear the cache
mTileCache.clear();
mCacheInitialized = false;
}
mCachingEnabled = enabled;
}
void Corkscrew::initializeCache() const {
if (!mCacheInitialized && mCachingEnabled) {
// Initialize cache with tiles for each LED position
mTileCache.resize(mNumLeds);
// Populate cache lazily
for (fl::size i = 0; i < mNumLeds; ++i) {
mTileCache[i] = calculateTileAtWrap(static_cast<float>(i));
}
mCacheInitialized = true;
}
}
CRGB* Corkscrew::rawData() {
// Use variant storage if available, otherwise fall back to input surface
if (!mPixelStorage.empty()) {
if (mPixelStorage.template is<fl::span<CRGB>>()) {
return mPixelStorage.template get<fl::span<CRGB>>().data();
} else if (mPixelStorage.template is<fl::vector<CRGB, fl::allocator_psram<CRGB>>>()) {
return mPixelStorage.template get<fl::vector<CRGB, fl::allocator_psram<CRGB>>>().data();
}
}
// Fall back to input surface data
auto surface = getOrCreateInputSurface();
return surface->data();
}
fl::span<CRGB> Corkscrew::data() {
// Use variant storage if available, otherwise fall back to input surface
if (!mPixelStorage.empty()) {
if (mPixelStorage.template is<fl::span<CRGB>>()) {
return mPixelStorage.template get<fl::span<CRGB>>();
} else if (mPixelStorage.template is<fl::vector<CRGB, fl::allocator_psram<CRGB>>>()) {
auto& vec = mPixelStorage.template get<fl::vector<CRGB, fl::allocator_psram<CRGB>>>();
return fl::span<CRGB>(vec.data(), vec.size());
}
}
// Fall back to input surface data as span
auto surface = getOrCreateInputSurface();
return fl::span<CRGB>(surface->data(), surface->size());
}
void Corkscrew::readFrom(const fl::Grid<CRGB>& source_grid, bool use_multi_sampling) {
if (use_multi_sampling) {
readFromMulti(source_grid);
return;
}
// Get or create the input surface
auto target_surface = getOrCreateInputSurface();
// Clear surface first
target_surface->clear();
// Iterate through each LED in the corkscrew
for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
// Get the rectangular coordinates for this corkscrew LED
vec2f rect_pos = at_no_wrap(static_cast<fl::u16>(led_idx));
// Convert to integer coordinates for indexing
vec2i16 coord(static_cast<fl::i16>(rect_pos.x + 0.5f),
static_cast<fl::i16>(rect_pos.y + 0.5f));
// Clamp coordinates to grid bounds
coord.x = MAX(0, MIN(coord.x, static_cast<fl::i16>(source_grid.width()) - 1));
coord.y = MAX(0, MIN(coord.y, static_cast<fl::i16>(source_grid.height()) - 1));
// Sample from the source fl::Grid using its at() method
CRGB sampled_color = source_grid.at(coord.x, coord.y);
// Store the sampled color directly in the target surface
if (led_idx < target_surface->size()) {
target_surface->data()[led_idx] = sampled_color;
}
}
}
void Corkscrew::clear() {
// Clear input surface if it exists
if (mInputSurface) {
mInputSurface->clear();
mInputSurface.reset(); // Free the shared_ptr memory
}
// Clear pixel storage if we own it (vector variant)
if (!mPixelStorage.empty()) {
if (mPixelStorage.template is<fl::vector<CRGB, fl::allocator_psram<CRGB>>>()) {
auto& vec = mPixelStorage.template get<fl::vector<CRGB, fl::allocator_psram<CRGB>>>();
vec.clear();
// Note: fl::vector doesn't have shrink_to_fit(), but clear() frees the memory
}
// Note: Don't clear external spans as we don't own that memory
}
// Clear tile cache
mTileCache.clear();
// Note: fl::vector doesn't have shrink_to_fit(), but clear() frees the memory
mCacheInitialized = false;
}
void Corkscrew::fillInputSurface(const CRGB& color) {
auto target_surface = getOrCreateInputSurface();
for (fl::size i = 0; i < target_surface->size(); ++i) {
target_surface->data()[i] = color;
}
}
void Corkscrew::draw(bool use_multi_sampling) {
// The draw method should map from the rectangular surface to the LED pixel data
// This is the reverse of readFrom - we read from our surface and populate LED data
auto source_surface = getOrCreateInputSurface();
// Make sure we have pixel storage
if (mPixelStorage.empty()) {
// If no pixel storage is configured, there's nothing to draw to
return;
}
CRGB* led_data = rawData();
if (!led_data) return;
if (use_multi_sampling) {
// Use multi-sampling to get better accuracy
for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
// Get the wrapped tile for this LED position
Tile2x2_u8_wrap tile = at_wrap(static_cast<float>(led_idx));
// Accumulate color from the 4 sample points with their weights
fl::u32 r_accum = 0, g_accum = 0, b_accum = 0;
fl::u32 total_weight = 0;
// Sample from each of the 4 corners of the tile
for (fl::u8 x = 0; x < 2; x++) {
for (fl::u8 y = 0; y < 2; y++) {
const auto& entry = tile.at(x, y);
vec2<u16> pos = entry.first;
fl::u8 weight = entry.second;
// Bounds check for the source surface
if (pos.x < source_surface->width() && pos.y < source_surface->height()) {
// Sample from the source surface
CRGB sample_color = source_surface->at(pos.x, pos.y);
// Accumulate weighted color components
r_accum += static_cast<fl::u32>(sample_color.r) * weight;
g_accum += static_cast<fl::u32>(sample_color.g) * weight;
b_accum += static_cast<fl::u32>(sample_color.b) * weight;
total_weight += weight;
}
}
}
// Calculate final color by dividing by total weight
CRGB final_color = CRGB::Black;
if (total_weight > 0) {
final_color.r = static_cast<fl::u8>(r_accum / total_weight);
final_color.g = static_cast<fl::u8>(g_accum / total_weight);
final_color.b = static_cast<fl::u8>(b_accum / total_weight);
}
// Store the result in the LED data
led_data[led_idx] = final_color;
}
} else {
// Simple non-multi-sampling version
for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
// Get the rectangular coordinates for this corkscrew LED
vec2f rect_pos = at_no_wrap(static_cast<fl::u16>(led_idx));
// Convert to integer coordinates for indexing
vec2i16 coord(static_cast<fl::i16>(rect_pos.x + 0.5f),
static_cast<fl::i16>(rect_pos.y + 0.5f));
// Clamp coordinates to surface bounds
coord.x = MAX(0, MIN(coord.x, static_cast<fl::i16>(source_surface->width()) - 1));
coord.y = MAX(0, MIN(coord.y, static_cast<fl::i16>(source_surface->height()) - 1));
// Sample from the source surface
CRGB sampled_color = source_surface->at(coord.x, coord.y);
// Store the sampled color in the LED data
led_data[led_idx] = sampled_color;
}
}
}
void Corkscrew::readFromMulti(const fl::Grid<CRGB>& source_grid) const {
// Get the target surface and clear it
auto target_surface = const_cast<Corkscrew*>(this)->getOrCreateInputSurface();
target_surface->clear();
const u16 width = static_cast<u16>(source_grid.width());
const u16 height = static_cast<u16>(source_grid.height());
// Iterate through each LED in the corkscrew
for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) {
// Get the wrapped tile for this LED position
Tile2x2_u8_wrap tile = at_wrap(static_cast<float>(led_idx));
// Accumulate color from the 4 sample points with their weights
fl::u32 r_accum = 0, g_accum = 0, b_accum = 0;
fl::u32 total_weight = 0;
// Sample from each of the 4 corners of the tile
for (fl::u8 x = 0; x < 2; x++) {
for (fl::u8 y = 0; y < 2; y++) {
const auto& entry = tile.at(x, y);
vec2<u16> pos = entry.first; // position is the first element of the pair
fl::u8 weight = entry.second; // weight is the second element of the pair
// Bounds check for the source grid
if (pos.x >= 0 && pos.x < width &&
pos.y >= 0 && pos.y < height) {
// Sample from the source grid
CRGB sample_color = source_grid.at(pos.x, pos.y);
// Accumulate weighted color components
r_accum += static_cast<fl::u32>(sample_color.r) * weight;
g_accum += static_cast<fl::u32>(sample_color.g) * weight;
b_accum += static_cast<fl::u32>(sample_color.b) * weight;
total_weight += weight;
}
}
}
// Calculate final color by dividing by total weight
CRGB final_color = CRGB::Black;
if (total_weight > 0) {
final_color.r = static_cast<fl::u8>(r_accum / total_weight);
final_color.g = static_cast<fl::u8>(g_accum / total_weight);
final_color.b = static_cast<fl::u8>(b_accum / total_weight);
}
// Store the result in the target surface at the LED index position
auto target_surface = const_cast<Corkscrew*>(this)->getOrCreateInputSurface();
if (led_idx < target_surface->size()) {
target_surface->data()[led_idx] = final_color;
}
}
}
// Iterator implementation
vec2f Corkscrew::iterator::operator*() const {
return corkscrew_->at_no_wrap(static_cast<fl::u16>(position_));
}
fl::ScreenMap Corkscrew::toScreenMap(float diameter) const {
// Create a ScreenMap with the correct number of LEDs
fl::ScreenMap screenMap(mNumLeds, diameter);
// For each LED index, calculate its position and set it in the ScreenMap
for (fl::u16 i = 0; i < mNumLeds; ++i) {
// Get the wrapped 2D position for this LED index in the cylindrical mapping
vec2f position = at_exact(i);
// Set the wrapped position in the ScreenMap
screenMap.set(i, position);
}
return screenMap;
}
// Enhanced surface handling methods
fl::shared_ptr<fl::Grid<CRGB>>& Corkscrew::getOrCreateInputSurface() {
if (!mInputSurface) {
// Create a new Grid with cylinder dimensions using PSRAM allocation
mInputSurface = fl::make_shared<fl::Grid<CRGB>>(mWidth, mHeight);
}
return mInputSurface;
}
fl::Grid<CRGB>& Corkscrew::surface() {
return *getOrCreateInputSurface();
}
fl::size Corkscrew::pixelCount() const {
// Use variant storage if available, otherwise fall back to legacy buffer size
if (!mPixelStorage.empty()) {
if (mPixelStorage.template is<fl::span<CRGB>>()) {
return mPixelStorage.template get<fl::span<CRGB>>().size();
} else if (mPixelStorage.template is<fl::vector<CRGB, fl::allocator_psram<CRGB>>>()) {
return mPixelStorage.template get<fl::vector<CRGB, fl::allocator_psram<CRGB>>>().size();
}
}
// Fall back to input size
return mNumLeds;
}
} // namespace fl

View File

@@ -0,0 +1,251 @@
#pragma once
/**
* @file corkscrew.h
* @brief Corkscrew LED strip projection and rendering
*
* The Corkscrew class provides a complete solution for drawing to densely-wrapped
* helical LED strips. It maps a cylindrical coordinate system to a linear LED
* strip, allowing you to draw on a rectangular surface and have it correctly
* projected onto the corkscrew topology.
*
* Usage:
* 1. Create a Corkscrew with the number of turns and LEDs
* 2. Draw patterns on the input surface using surface()
* 3. Call draw() to map the surface to LED pixels
* 4. Access the final LED data via rawData()
*
* The class handles:
* - Automatic cylindrical dimension calculation
* - Pixel storage (external span or internal allocation)
* - Multi-sampling for smooth projections
* - Gap compensation for non-continuous wrapping
* - Iterator interface for advanced coordinate access
*
* Parameters:
* - totalTurns: Number of helical turns around the cylinder
* - numLeds: Total number of LEDs in the strip
* - invert: Reverse the mapping direction (default: false)
* - gapParams: Optional gap compensation for solder points in a strip.
*/
#include "fl/allocator.h"
#include "fl/geometry.h"
#include "fl/math.h"
#include "fl/math_macros.h"
#include "fl/pair.h"
#include "fl/tile2x2.h"
#include "fl/vector.h"
#include "fl/shared_ptr.h"
#include "fl/variant.h"
#include "fl/span.h"
#include "crgb.h"
#include "fl/int.h"
namespace fl {
// Forward declarations
class Leds;
class ScreenMap;
template<typename T> class Grid;
// Simple constexpr functions for compile-time corkscrew dimension calculation
constexpr fl::u16 calculateCorkscrewWidth(float totalTurns, fl::u16 numLeds) {
return static_cast<fl::u16>(ceil_constexpr(static_cast<float>(numLeds) / totalTurns));
}
constexpr fl::u16 calculateCorkscrewHeight(float totalTurns, fl::u16 numLeds) {
return (calculateCorkscrewWidth(totalTurns, numLeds) * static_cast<int>(ceil_constexpr(totalTurns)) > numLeds) ?
static_cast<fl::u16>(ceil_constexpr(static_cast<float>(numLeds) / static_cast<float>(calculateCorkscrewWidth(totalTurns, numLeds)))) :
static_cast<fl::u16>(ceil_constexpr(totalTurns));
}
/**
* Struct representing gap parameters for corkscrew mapping
*/
struct Gap {
int num_leds = 0; // Number of LEDs after which gap is activated, 0 = no gap
float gap = 0.0f; // Gap value from 0 to 1, represents percentage of width unit to add
Gap() = default;
Gap(float g) : num_leds(0), gap(g) {} // Backwards compatibility constructor
Gap(int n, float g) : num_leds(n), gap(g) {} // New constructor with num_leds
// Rule of 5 for POD data
Gap(const Gap &other) = default;
Gap &operator=(const Gap &other) = default;
Gap(Gap &&other) noexcept = default;
Gap &operator=(Gap &&other) noexcept = default;
};
// Maps a Corkscrew defined by the input to a cylindrical mapping for rendering
// a densly wrapped LED corkscrew.
class Corkscrew {
public:
// Pixel storage variants - can hold either external span or owned vector
using PixelStorage = fl::Variant<fl::span<CRGB>, fl::vector<CRGB, fl::allocator_psram<CRGB>>>;
// Iterator class moved from CorkscrewState
class iterator {
public:
using value_type = vec2f;
using difference_type = fl::i32;
using pointer = vec2f *;
using reference = vec2f &;
iterator(const Corkscrew *corkscrew, fl::size position)
: corkscrew_(corkscrew), position_(position) {}
vec2f operator*() const;
iterator &operator++() {
++position_;
return *this;
}
iterator operator++(int) {
iterator temp = *this;
++position_;
return temp;
}
iterator &operator--() {
--position_;
return *this;
}
iterator operator--(int) {
iterator temp = *this;
--position_;
return temp;
}
bool operator==(const iterator &other) const {
return position_ == other.position_;
}
bool operator!=(const iterator &other) const {
return position_ != other.position_;
}
difference_type operator-(const iterator &other) const {
return static_cast<difference_type>(position_) -
static_cast<difference_type>(other.position_);
}
private:
const Corkscrew *corkscrew_;
fl::size position_;
};
// Constructors that integrate input parameters directly
// Primary constructor with default values for invert and gapParams
Corkscrew(float totalTurns, fl::u16 numLeds, bool invert = false, const Gap& gapParams = Gap());
// Constructor with external pixel buffer - these pixels will be drawn to directly
Corkscrew(float totalTurns, fl::span<CRGB> dstPixels, bool invert = false, const Gap& gapParams = Gap());
Corkscrew(const Corkscrew &) = default;
Corkscrew(Corkscrew &&) = default;
// Caching control
void setCachingEnabled(bool enabled);
// Essential API - Core functionality
fl::u16 cylinderWidth() const { return mWidth; }
fl::u16 cylinderHeight() const { return mHeight; }
// Enhanced surface handling with shared_ptr
// Note: Input surface will be created on first call
fl::shared_ptr<fl::Grid<CRGB>>& getOrCreateInputSurface();
// Draw like a regular rectangle surface - access input surface directly
fl::Grid<CRGB>& surface();
// Draw the corkscrew by reading from the internal surface and populating LED pixels
void draw(bool use_multi_sampling = true);
// Pixel storage access - works with both external and owned pixels
// This represents the pixels that will be drawn after draw() is called
CRGB* rawData();
// Returns span of pixels that will be written to when draw() is called
fl::span<CRGB> data();
fl::size pixelCount() const;
// Create and return a fully constructed ScreenMap for this corkscrew
// Each LED index will be mapped to its exact position on the cylindrical surface
fl::ScreenMap toScreenMap(float diameter = 0.5f) const;
// STL-style container interface
fl::size size() const;
iterator begin() { return iterator(this, 0); }
iterator end() { return iterator(this, size()); }
// Non-essential API - Lower level access
vec2f at_no_wrap(fl::u16 i) const;
vec2f at_exact(fl::u16 i) const;
Tile2x2_u8_wrap at_wrap(float i) const;
// Clear all buffers and free memory
void clear();
// Fill the input surface with a color
void fillInputSurface(const CRGB& color);
private:
// For internal use. Splats the pixel on the surface which
// extends past the width. This extended Tile2x2 is designed
// to be wrapped around with a Tile2x2_u8_wrap.
Tile2x2_u8 at_splat_extrapolate(float i) const;
// Read from fl::Grid<CRGB> object and populate our internal rectangular buffer
// by sampling from the XY coordinates mapped to each corkscrew LED position
// use_multi_sampling = true will use multi-sampling to sample from the source grid,
// this will give a little bit better accuracy and the screenmap will be more accurate.
void readFrom(const fl::Grid<CRGB>& source_grid, bool use_multi_sampling = true);
// Read from rectangular buffer using multi-sampling and store in target grid
// Uses Tile2x2_u8_wrap for sub-pixel accurate sampling with proper blending
void readFromMulti(const fl::Grid<CRGB>& target_grid) const;
// Initialize the rectangular buffer if not already done
void initializeBuffer() const;
// Initialize the cache if not already done and caching is enabled
void initializeCache() const;
// Calculate the tile at position i without using cache
Tile2x2_u8_wrap calculateTileAtWrap(float i) const;
// Core corkscrew parameters (moved from CorkscrewInput)
float mTotalTurns = 19.0f; // Total turns of the corkscrew
fl::u16 mNumLeds = 144; // Number of LEDs
Gap mGapParams; // Gap parameters for gap accounting
bool mInvert = false; // If true, reverse the mapping order
// Cylindrical mapping dimensions (moved from CorkscrewState)
fl::u16 mWidth = 0; // Width of cylindrical map (circumference of one turn)
fl::u16 mHeight = 0; // Height of cylindrical map (total vertical segments)
// Enhanced pixel storage - variant supports both external and owned pixels
PixelStorage mPixelStorage;
bool mOwnsPixels = false; // Track whether we own the pixel data
// Input surface for drawing operations
fl::shared_ptr<fl::Grid<CRGB>> mInputSurface;
// Caching for Tile2x2_u8_wrap objects
mutable fl::vector<Tile2x2_u8_wrap> mTileCache;
mutable bool mCacheInitialized = false;
bool mCachingEnabled = true; // Default to enabled
};
} // namespace fl

View File

@@ -0,0 +1,31 @@
/// @file crgb_hsv16.cpp
/// HSV16-dependent methods for CRGB - only linked when HSV16 functionality is used
#define FASTLED_INTERNAL
#include "crgb.h"
#include "fl/hsv16.h"
#include "fl/namespace.h"
FASTLED_NAMESPACE_BEGIN
CRGB CRGB::colorBoost(fl::EaseType saturation_function, fl::EaseType luminance_function) const {
fl::HSV16 hsv(*this);
return hsv.colorBoost(saturation_function, luminance_function);
}
void CRGB::colorBoost(const CRGB* src, CRGB* dst, size_t count, fl::EaseType saturation_function, fl::EaseType luminance_function) {
for (size_t i = 0; i < count; i++) {
dst[i] = src[i].colorBoost(saturation_function, luminance_function);
}
}
fl::HSV16 CRGB::toHSV16() const {
return fl::HSV16(*this);
}
// Constructor implementation for HSV16 -> CRGB automatic conversion
CRGB::CRGB(const fl::HSV16& rhs) {
*this = rhs.ToRGB();
}
FASTLED_NAMESPACE_END

View File

@@ -0,0 +1,25 @@
#pragma once
#ifdef FASTLED_TESTING
#include <cstddef> // ok include
#endif
namespace fl {
// FastLED equivalent of std::nullptr_t
typedef decltype(nullptr) nullptr_t;
// FastLED equivalent of std::size_t and std::ptrdiff_t
// These are defined here for completeness but may already exist elsewhere
#ifndef FL_SIZE_T_DEFINED
#define FL_SIZE_T_DEFINED
typedef __SIZE_TYPE__ size_t;
#endif
#ifndef FL_PTRDIFF_T_DEFINED
#define FL_PTRDIFF_T_DEFINED
typedef __PTRDIFF_TYPE__ ptrdiff_t;
#endif
} // namespace fl

View File

@@ -0,0 +1,67 @@
#pragma once
#include "fl/strstream.h"
#include "fl/sketch_macros.h"
#include "fl/int.h"
#include "fl/stdint.h"
// Forward declaration to avoid pulling in fl/io.h and causing fl/io.cpp to be compiled
// This prevents ~5KB memory bloat for simple applications
#ifndef FL_DBG_PRINTLN_DECLARED
#define FL_DBG_PRINTLN_DECLARED
namespace fl {
void println(const char* str);
}
#endif
namespace fl {
// ".build/src/fl/dbg.h" -> "src/fl/dbg.h"
// "blah/blah/blah.h" -> "blah.h"
inline const char *fastled_file_offset(const char *file) {
const char *p = file;
const char *last_slash = nullptr;
while (*p) {
if (p[0] == 's' && p[1] == 'r' && p[2] == 'c' && p[3] == '/') {
return p; // Skip past "src/"
}
if (*p == '/') { // fallback to using last slash
last_slash = p;
}
p++;
}
// If "src/" not found but we found at least one slash, return after the
// last slash
if (last_slash) {
return last_slash + 1;
}
return file; // If no slashes found at all, return original path
}
} // namespace fl
#if __EMSCRIPTEN__ || !defined(RELEASE) || defined(FASTLED_TESTING)
#define FASTLED_FORCE_DBG 1
#endif
// Debug printing: Enable only when explicitly requested to avoid ~5KB memory bloat
#if !defined(FASTLED_FORCE_DBG) || !SKETCH_HAS_LOTS_OF_MEMORY
// By default, debug printing is disabled to prevent memory bloat in simple applications
#define FASTLED_HAS_DBG 0
#define _FASTLED_DGB(X) do { if (false) { fl::println(""); } } while(0) // No-op that handles << operator
#else
// Explicit debug mode enabled - uses fl::println()
#define FASTLED_HAS_DBG 1
#define _FASTLED_DGB(X) \
fl::println( \
(fl::StrStream() << (fl::fastled_file_offset(__FILE__)) \
<< "(" << int(__LINE__) << "): " << X) \
.c_str())
#endif
#define FASTLED_DBG(X) _FASTLED_DGB(X)
#ifndef FASTLED_DBG_IF
#define FASTLED_DBG_IF(COND, MSG) \
if (COND) \
FASTLED_DBG(MSG)
#endif // FASTLED_DBG_IF

View File

@@ -0,0 +1,28 @@
#pragma once
#if defined(__clang__)
// Clang: Do not mark classes as deprecated
#define FASTLED_DEPRECATED_CLASS(msg)
#ifndef FASTLED_DEPRECATED
#define FASTLED_DEPRECATED(msg) __attribute__((deprecated(msg)))
#endif
#elif defined(__GNUC__) // GCC (but not Clang)
#ifndef FASTLED_DEPRECATED
#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)
#define FASTLED_DEPRECATED(msg) __attribute__((deprecated(msg)))
#define FASTLED_DEPRECATED_CLASS(msg) __attribute__((deprecated(msg)))
#else
#define FASTLED_DEPRECATED(msg) __attribute__((deprecated))
#define FASTLED_DEPRECATED_CLASS(msg)
#endif
#endif
#else // Other compilers
#ifndef FASTLED_DEPRECATED
#define FASTLED_DEPRECATED(msg)
#endif
#define FASTLED_DEPRECATED_CLASS(msg)
#endif
#define FL_DEPRECATED(msg) FASTLED_DEPRECATED(msg)
#define FL_DEPRECATED_CLASS(msg) FASTLED_DEPRECATED_CLASS(msg)

View File

@@ -0,0 +1,390 @@
#pragma once
#include "fl/stdint.h"
#include <string.h>
#include "fl/allocator.h"
#include "fl/initializer_list.h"
#include "fl/inplacenew.h"
#include "fl/move.h"
#include "fl/namespace.h"
#include "fl/type_traits.h"
namespace fl {
template <typename T, typename Allocator = fl::allocator<T>>
class deque {
private:
T* mData = nullptr;
fl::size mCapacity = 0;
fl::size mSize = 0;
fl::size mFront = 0; // Index of the front element
Allocator mAlloc;
static const fl::size kInitialCapacity = 8;
void ensure_capacity(fl::size min_capacity) {
if (mCapacity >= min_capacity) {
return;
}
fl::size new_capacity = mCapacity == 0 ? kInitialCapacity : mCapacity * 2;
while (new_capacity < min_capacity) {
new_capacity *= 2;
}
T* new_data = mAlloc.allocate(new_capacity);
if (!new_data) {
return; // Allocation failed
}
// Copy existing elements to new buffer in linear order
for (fl::size i = 0; i < mSize; ++i) {
fl::size old_idx = (mFront + i) % mCapacity;
mAlloc.construct(&new_data[i], fl::move(mData[old_idx]));
mAlloc.destroy(&mData[old_idx]);
}
if (mData) {
mAlloc.deallocate(mData, mCapacity);
}
mData = new_data;
mCapacity = new_capacity;
mFront = 0; // Reset front to 0 after reallocation
}
fl::size get_index(fl::size logical_index) const {
return (mFront + logical_index) % mCapacity;
}
public:
// Iterator implementation
class iterator {
private:
deque* mDeque;
fl::size mIndex;
public:
iterator(deque* dq, fl::size index) : mDeque(dq), mIndex(index) {}
T& operator*() const {
return (*mDeque)[mIndex];
}
T* operator->() const {
return &(*mDeque)[mIndex];
}
iterator& operator++() {
++mIndex;
return *this;
}
iterator operator++(int) {
iterator temp = *this;
++mIndex;
return temp;
}
iterator& operator--() {
--mIndex;
return *this;
}
iterator operator--(int) {
iterator temp = *this;
--mIndex;
return temp;
}
bool operator==(const iterator& other) const {
return mDeque == other.mDeque && mIndex == other.mIndex;
}
bool operator!=(const iterator& other) const {
return !(*this == other);
}
};
class const_iterator {
private:
const deque* mDeque;
fl::size mIndex;
public:
const_iterator(const deque* dq, fl::size index) : mDeque(dq), mIndex(index) {}
const T& operator*() const {
return (*mDeque)[mIndex];
}
const T* operator->() const {
return &(*mDeque)[mIndex];
}
const_iterator& operator++() {
++mIndex;
return *this;
}
const_iterator operator++(int) {
const_iterator temp = *this;
++mIndex;
return temp;
}
const_iterator& operator--() {
--mIndex;
return *this;
}
const_iterator operator--(int) {
const_iterator temp = *this;
--mIndex;
return temp;
}
bool operator==(const const_iterator& other) const {
return mDeque == other.mDeque && mIndex == other.mIndex;
}
bool operator!=(const const_iterator& other) const {
return !(*this == other);
}
};
// Constructors
deque() : mData(nullptr), mCapacity(0), mSize(0), mFront(0) {}
explicit deque(fl::size count, const T& value = T()) : deque() {
resize(count, value);
}
deque(const deque& other) : deque() {
*this = other;
}
deque(deque&& other) : deque() {
*this = fl::move(other);
}
deque(fl::initializer_list<T> init) : deque() {
for (const auto& value : init) {
push_back(value);
}
}
// Destructor
~deque() {
clear();
if (mData) {
mAlloc.deallocate(mData, mCapacity);
}
}
// Assignment operators
deque& operator=(const deque& other) {
if (this != &other) {
clear();
for (fl::size i = 0; i < other.size(); ++i) {
push_back(other[i]);
}
}
return *this;
}
deque& operator=(deque&& other) {
if (this != &other) {
clear();
if (mData) {
mAlloc.deallocate(mData, mCapacity);
}
mData = other.mData;
mCapacity = other.mCapacity;
mSize = other.mSize;
mFront = other.mFront;
mAlloc = other.mAlloc;
other.mData = nullptr;
other.mCapacity = 0;
other.mSize = 0;
other.mFront = 0;
}
return *this;
}
// Element access
T& operator[](fl::size index) {
return mData[get_index(index)];
}
const T& operator[](fl::size index) const {
return mData[get_index(index)];
}
T& at(fl::size index) {
if (index >= mSize) {
// Handle bounds error - in embedded context, we'll just return the first element
// In a real implementation, this might throw an exception
return mData[mFront];
}
return mData[get_index(index)];
}
const T& at(fl::size index) const {
if (index >= mSize) {
// Handle bounds error - in embedded context, we'll just return the first element
return mData[mFront];
}
return mData[get_index(index)];
}
T& front() {
return mData[mFront];
}
const T& front() const {
return mData[mFront];
}
T& back() {
return mData[get_index(mSize - 1)];
}
const T& back() const {
return mData[get_index(mSize - 1)];
}
// Iterators
iterator begin() {
return iterator(this, 0);
}
const_iterator begin() const {
return const_iterator(this, 0);
}
iterator end() {
return iterator(this, mSize);
}
const_iterator end() const {
return const_iterator(this, mSize);
}
// Capacity
bool empty() const {
return mSize == 0;
}
fl::size size() const {
return mSize;
}
fl::size capacity() const {
return mCapacity;
}
// Modifiers
void clear() {
while (!empty()) {
pop_back();
}
}
void push_back(const T& value) {
ensure_capacity(mSize + 1);
fl::size back_index = get_index(mSize);
mAlloc.construct(&mData[back_index], value);
++mSize;
}
void push_back(T&& value) {
ensure_capacity(mSize + 1);
fl::size back_index = get_index(mSize);
mAlloc.construct(&mData[back_index], fl::move(value));
++mSize;
}
void push_front(const T& value) {
ensure_capacity(mSize + 1);
mFront = (mFront - 1 + mCapacity) % mCapacity;
mAlloc.construct(&mData[mFront], value);
++mSize;
}
void push_front(T&& value) {
ensure_capacity(mSize + 1);
mFront = (mFront - 1 + mCapacity) % mCapacity;
mAlloc.construct(&mData[mFront], fl::move(value));
++mSize;
}
void pop_back() {
if (mSize > 0) {
fl::size back_index = get_index(mSize - 1);
mAlloc.destroy(&mData[back_index]);
--mSize;
}
}
void pop_front() {
if (mSize > 0) {
mAlloc.destroy(&mData[mFront]);
mFront = (mFront + 1) % mCapacity;
--mSize;
}
}
void resize(fl::size new_size) {
resize(new_size, T());
}
void resize(fl::size new_size, const T& value) {
if (new_size > mSize) {
// Add elements
ensure_capacity(new_size);
while (mSize < new_size) {
push_back(value);
}
} else if (new_size < mSize) {
// Remove elements
while (mSize > new_size) {
pop_back();
}
}
// If new_size == mSize, do nothing
}
void swap(deque& other) {
if (this != &other) {
T* temp_data = mData;
fl::size temp_capacity = mCapacity;
fl::size temp_size = mSize;
fl::size temp_front = mFront;
Allocator temp_alloc = mAlloc;
mData = other.mData;
mCapacity = other.mCapacity;
mSize = other.mSize;
mFront = other.mFront;
mAlloc = other.mAlloc;
other.mData = temp_data;
other.mCapacity = temp_capacity;
other.mSize = temp_size;
other.mFront = temp_front;
other.mAlloc = temp_alloc;
}
}
};
// Convenience typedef for the most common use case
typedef deque<int> deque_int;
typedef deque<float> deque_float;
typedef deque<double> deque_double;
} // namespace fl

View File

@@ -0,0 +1,53 @@
#pragma once
/// @file dll.h
/// FastLED dynamic library interface - lightweight header for external callers
#ifndef FASTLED_BUILD_EXPORTS
#define FASTLED_BUILD_EXPORTS 0
#endif
#if FASTLED_BUILD_EXPORTS
#include "export.h"
#ifdef __cplusplus
extern "C" {
#endif
/// Call the sketch's setup() function
/// @note This is the C ABI export for external sketch runners
FASTLED_EXPORT void sketch_setup(void);
/// Call the sketch's loop() function
/// @note This is the C ABI export for external sketch runners
FASTLED_EXPORT void sketch_loop(void);
#ifdef __cplusplus
}
#endif
// ================================================================================================
// IMPLEMENTATIONS (when building FastLED as shared library)
// ================================================================================================
#ifdef FASTLED_LIBRARY_SHARED
#ifdef __cplusplus
// Forward declarations - provided by sketch
extern void setup();
extern void loop();
// Provide implementations for the exported functions
FASTLED_EXPORT void sketch_setup() {
setup();
}
FASTLED_EXPORT void sketch_loop() {
loop();
}
#endif // __cplusplus
#endif // FASTLED_LIBRARY_SHARED
#endif // FASTLED_BUILD_EXPORTS

View File

@@ -0,0 +1,188 @@
// BETA - NOT TESTED!!!
// VIBE CODED WITH AI
#include "fl/downscale.h"
#include "fl/int.h"
#include "crgb.h"
#include "fl/assert.h"
#include "fl/math_macros.h"
#include "fl/xymap.h"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshift-count-overflow"
namespace fl {
void downscaleHalf(const CRGB *src, fl::u16 srcWidth, fl::u16 srcHeight,
CRGB *dst) {
fl::u16 dstWidth = srcWidth / 2;
fl::u16 dstHeight = srcHeight / 2;
for (fl::u16 y = 0; y < dstHeight; ++y) {
for (fl::u16 x = 0; x < dstWidth; ++x) {
// Map to top-left of the 2x2 block in source
fl::u16 srcX = x * 2;
fl::u16 srcY = y * 2;
// Fetch 2x2 block
const CRGB &p00 = src[(srcY)*srcWidth + (srcX)];
const CRGB &p10 = src[(srcY)*srcWidth + (srcX + 1)];
const CRGB &p01 = src[(srcY + 1) * srcWidth + (srcX)];
const CRGB &p11 = src[(srcY + 1) * srcWidth + (srcX + 1)];
// Average each color channel
fl::u16 r =
(p00.r + p10.r + p01.r + p11.r + 2) / 4; // +2 for rounding
fl::u16 g = (p00.g + p10.g + p01.g + p11.g + 2) / 4;
fl::u16 b = (p00.b + p10.b + p01.b + p11.b + 2) / 4;
// Store result
dst[y * dstWidth + x] = CRGB((fl::u8)r, (fl::u8)g, (fl::u8)b);
}
}
}
void downscaleHalf(const CRGB *src, const XYMap &srcXY, CRGB *dst,
const XYMap &dstXY) {
fl::u16 dstWidth = dstXY.getWidth();
fl::u16 dstHeight = dstXY.getHeight();
FASTLED_ASSERT(srcXY.getWidth() == dstXY.getWidth() * 2,
"Source width must be double the destination width");
FASTLED_ASSERT(srcXY.getHeight() == dstXY.getHeight() * 2,
"Source height must be double the destination height");
for (fl::u16 y = 0; y < dstHeight; ++y) {
for (fl::u16 x = 0; x < dstWidth; ++x) {
// Map to top-left of the 2x2 block in source
fl::u16 srcX = x * 2;
fl::u16 srcY = y * 2;
// Fetch 2x2 block
const CRGB &p00 = src[srcXY.mapToIndex(srcX, srcY)];
const CRGB &p10 = src[srcXY.mapToIndex(srcX + 1, srcY)];
const CRGB &p01 = src[srcXY.mapToIndex(srcX, srcY + 1)];
const CRGB &p11 = src[srcXY.mapToIndex(srcX + 1, srcY + 1)];
// Average each color channel
fl::u16 r =
(p00.r + p10.r + p01.r + p11.r + 2) / 4; // +2 for rounding
fl::u16 g = (p00.g + p10.g + p01.g + p11.g + 2) / 4;
fl::u16 b = (p00.b + p10.b + p01.b + p11.b + 2) / 4;
// Store result
dst[dstXY.mapToIndex(x, y)] =
CRGB((fl::u8)r, (fl::u8)g, (fl::u8)b);
}
}
}
void downscaleArbitrary(const CRGB *src, const XYMap &srcXY, CRGB *dst,
const XYMap &dstXY) {
const fl::u16 srcWidth = srcXY.getWidth();
const fl::u16 srcHeight = srcXY.getHeight();
const fl::u16 dstWidth = dstXY.getWidth();
const fl::u16 dstHeight = dstXY.getHeight();
const fl::u32 FP_ONE = 256; // Q8.8 fixed-point multiplier
FASTLED_ASSERT(dstWidth <= srcWidth,
"Destination width must be <= source width");
FASTLED_ASSERT(dstHeight <= srcHeight,
"Destination height must be <= source height");
for (fl::u16 dy = 0; dy < dstHeight; ++dy) {
// Fractional boundaries in Q8.8
fl::u32 dstY0 = (dy * srcHeight * FP_ONE) / dstHeight;
fl::u32 dstY1 = ((dy + 1) * srcHeight * FP_ONE) / dstHeight;
for (fl::u16 dx = 0; dx < dstWidth; ++dx) {
fl::u32 dstX0 = (dx * srcWidth * FP_ONE) / dstWidth;
fl::u32 dstX1 = ((dx + 1) * srcWidth * FP_ONE) / dstWidth;
fl::u64 rSum = 0, gSum = 0, bSum = 0;
fl::u32 totalWeight = 0;
// Find covered source pixels
fl::u16 srcY_start = dstY0 / FP_ONE;
fl::u16 srcY_end = (dstY1 + FP_ONE - 1) / FP_ONE; // ceil
fl::u16 srcX_start = dstX0 / FP_ONE;
fl::u16 srcX_end = (dstX1 + FP_ONE - 1) / FP_ONE; // ceil
for (fl::u16 sy = srcY_start; sy < srcY_end; ++sy) {
// Calculate vertical overlap in Q8.8
fl::u32 sy0 = sy * FP_ONE;
fl::u32 sy1 = (sy + 1) * FP_ONE;
fl::u32 y_overlap = MIN(dstY1, sy1) - MAX(dstY0, sy0);
if (y_overlap == 0)
continue;
for (fl::u16 sx = srcX_start; sx < srcX_end; ++sx) {
fl::u32 sx0 = sx * FP_ONE;
fl::u32 sx1 = (sx + 1) * FP_ONE;
fl::u32 x_overlap = MIN(dstX1, sx1) - MAX(dstX0, sx0);
if (x_overlap == 0)
continue;
fl::u32 weight = (x_overlap * y_overlap + (FP_ONE >> 1)) >>
8; // Q8.8 * Q8.8 → Q16.16 → Q8.8
const CRGB &p = src[srcXY.mapToIndex(sx, sy)];
rSum += p.r * weight;
gSum += p.g * weight;
bSum += p.b * weight;
totalWeight += weight;
}
}
// Final division, rounding
fl::u8 r =
totalWeight ? (rSum + (totalWeight >> 1)) / totalWeight : 0;
fl::u8 g =
totalWeight ? (gSum + (totalWeight >> 1)) / totalWeight : 0;
fl::u8 b =
totalWeight ? (bSum + (totalWeight >> 1)) / totalWeight : 0;
dst[dstXY.mapToIndex(dx, dy)] = CRGB(r, g, b);
}
}
}
void downscale(const CRGB *src, const XYMap &srcXY, CRGB *dst,
const XYMap &dstXY) {
fl::u16 srcWidth = srcXY.getWidth();
fl::u16 srcHeight = srcXY.getHeight();
fl::u16 dstWidth = dstXY.getWidth();
fl::u16 dstHeight = dstXY.getHeight();
FASTLED_ASSERT(dstWidth <= srcWidth,
"Destination width must be <= source width");
FASTLED_ASSERT(dstHeight <= srcHeight,
"Destination height must be <= source height");
const bool destination_is_half_of_source =
(dstWidth * 2 == srcWidth) && (dstHeight * 2 == srcHeight);
// Attempt to use the downscaleHalf function if the destination is half the
// size of the source.
if (destination_is_half_of_source) {
const bool both_rectangles = (srcXY.getType() == XYMap::kLineByLine) &&
(dstXY.getType() == XYMap::kLineByLine);
if (both_rectangles) {
// If both source and destination are rectangular, we can use the
// optimized version
downscaleHalf(src, srcWidth, srcHeight, dst);
} else {
// Otherwise, we need to use the mapped version
downscaleHalf(src, srcXY, dst, dstXY);
}
return;
}
downscaleArbitrary(src, srcXY, dst, dstXY);
}
} // namespace fl
#pragma GCC diagnostic pop

View File

@@ -0,0 +1,33 @@
#pragma once
/*
Experimental bilinearn downscaling algorithm. Not tested yet and completely
"vibe-coded" by ai.
If you use this and find an issue then please report it.
*/
#include "fl/int.h"
#include "crgb.h"
namespace fl {
class XYMap;
void downscale(const CRGB *src, const XYMap &srcXY, CRGB *dst,
const XYMap &dstXY);
// Optimized versions for downscaling by 50%. This is here for testing purposes
// mostly. You should prefer to use downscale(...) instead of calling these
// functions. It's important to note that downscale(...) will invoke
// downscaleHalf(...) automatically when the source and destination are half the
// size of each other.
void downscaleHalf(const CRGB *src, fl::u16 srcWidth, fl::u16 srcHeight,
CRGB *dst);
void downscaleHalf(const CRGB *src, const XYMap &srcXY, CRGB *dst,
const XYMap &dstXY);
void downscaleArbitrary(const CRGB *src, const XYMap &srcXY, CRGB *dst,
const XYMap &dstXY);
} // namespace fl

View File

@@ -0,0 +1,7 @@
#pragma once
namespace fl {
enum DrawMode { DRAW_MODE_OVERWRITE, DRAW_MODE_BLEND_BY_MAX_BRIGHTNESS };
} // namespace fl

View File

@@ -0,0 +1,77 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
#include "crgb.h"
#include "fl/geometry.h"
#include "fl/gradient.h"
#include "fl/namespace.h"
#include "fl/unused.h"
#include "fl/xymap.h"
#include "fl/move.h"
namespace fl {
// Draws a fl::u8 value to a CRGB array, blending it with the existing color.
struct XYDrawComposited {
XYDrawComposited(const CRGB &color, const XYMap &xymap, CRGB *out);
// Copy constructor (assignment deleted due to const members)
XYDrawComposited(const XYDrawComposited &other) = default;
XYDrawComposited &operator=(const XYDrawComposited &other) = delete;
// Move constructor (assignment deleted due to const members)
XYDrawComposited(XYDrawComposited &&other) noexcept
: mColor(fl::move(other.mColor)), mXYMap(fl::move(other.mXYMap)), mOut(other.mOut) {}
XYDrawComposited &operator=(XYDrawComposited &&other) noexcept = delete;
void draw(const vec2<fl::u16> &pt, fl::u32 index, fl::u8 value);
const CRGB mColor;
const XYMap mXYMap;
CRGB *mOut;
};
struct XYDrawGradient {
XYDrawGradient(const Gradient &gradient, const XYMap &xymap, CRGB *out);
// Copy constructor (assignment deleted due to const members)
XYDrawGradient(const XYDrawGradient &other) = default;
XYDrawGradient &operator=(const XYDrawGradient &other) = delete;
// Move constructor (assignment deleted due to const members)
XYDrawGradient(XYDrawGradient &&other) noexcept
: mGradient(fl::move(other.mGradient)), mXYMap(fl::move(other.mXYMap)), mOut(other.mOut) {}
XYDrawGradient &operator=(XYDrawGradient &&other) noexcept = delete;
void draw(const vec2<fl::u16> &pt, fl::u32 index, fl::u8 value);
const Gradient mGradient;
const XYMap mXYMap;
CRGB *mOut;
};
inline XYDrawComposited::XYDrawComposited(const CRGB &color, const XYMap &xymap,
CRGB *out)
: mColor(color), mXYMap(xymap), mOut(out) {}
inline void XYDrawComposited::draw(const vec2<fl::u16> &pt, fl::u32 index,
fl::u8 value) {
FASTLED_UNUSED(pt);
CRGB &c = mOut[index];
CRGB blended = mColor;
blended.fadeToBlackBy(255 - value);
c = CRGB::blendAlphaMaxChannel(blended, c);
}
inline XYDrawGradient::XYDrawGradient(const Gradient &gradient,
const XYMap &xymap, CRGB *out)
: mGradient(gradient), mXYMap(xymap), mOut(out) {}
inline void XYDrawGradient::draw(const vec2<fl::u16> &pt, fl::u32 index,
fl::u8 value) {
FASTLED_UNUSED(pt);
CRGB c = mGradient.colorAt(value);
mOut[index] = c;
}
} // namespace fl

View File

@@ -0,0 +1,339 @@
#ifndef FASTLED_INTERNAL
#define FASTLED_INTERNAL
#endif
#include "FastLED.h"
#include "fl/ease.h"
#include "lib8tion.h" // This is the problematic header that's hard to include
#include "fl/map_range.h"
#include "lib8tion/intmap.h"
#include "fl/sin32.h"
#include "fl/int.h"
namespace fl {
// Gamma 2.8 lookup table for 8-bit to 16-bit gamma correction
// This table converts linear 8-bit values to gamma-corrected 16-bit values
// using a gamma curve of 2.8 (commonly used for LED brightness correction)
const u16 gamma_2_8[256] FL_PROGMEM = {
0, 0, 0, 1, 1, 2, 4, 6, 8, 11,
14, 18, 23, 29, 35, 41, 49, 57, 67, 77,
88, 99, 112, 126, 141, 156, 173, 191, 210, 230,
251, 274, 297, 322, 348, 375, 404, 433, 464, 497,
531, 566, 602, 640, 680, 721, 763, 807, 853, 899,
948, 998, 1050, 1103, 1158, 1215, 1273, 1333, 1394, 1458,
1523, 1590, 1658, 1729, 1801, 1875, 1951, 2029, 2109, 2190,
2274, 2359, 2446, 2536, 2627, 2720, 2816, 2913, 3012, 3114,
3217, 3323, 3431, 3541, 3653, 3767, 3883, 4001, 4122, 4245,
4370, 4498, 4627, 4759, 4893, 5030, 5169, 5310, 5453, 5599,
5747, 5898, 6051, 6206, 6364, 6525, 6688, 6853, 7021, 7191,
7364, 7539, 7717, 7897, 8080, 8266, 8454, 8645, 8838, 9034,
9233, 9434, 9638, 9845, 10055, 10267, 10482, 10699, 10920, 11143,
11369, 11598, 11829, 12064, 12301, 12541, 12784, 13030, 13279, 13530,
13785, 14042, 14303, 14566, 14832, 15102, 15374, 15649, 15928, 16209,
16493, 16781, 17071, 17365, 17661, 17961, 18264, 18570, 18879, 19191,
19507, 19825, 20147, 20472, 20800, 21131, 21466, 21804, 22145, 22489,
22837, 23188, 23542, 23899, 24260, 24625, 24992, 25363, 25737, 26115,
26496, 26880, 27268, 27659, 28054, 28452, 28854, 29259, 29667, 30079,
30495, 30914, 31337, 31763, 32192, 32626, 33062, 33503, 33947, 34394,
34846, 35300, 35759, 36221, 36687, 37156, 37629, 38106, 38586, 39071,
39558, 40050, 40545, 41045, 41547, 42054, 42565, 43079, 43597, 44119,
44644, 45174, 45707, 46245, 46786, 47331, 47880, 48432, 48989, 49550,
50114, 50683, 51255, 51832, 52412, 52996, 53585, 54177, 54773, 55374,
55978, 56587, 57199, 57816, 58436, 59061, 59690, 60323, 60960, 61601,
62246, 62896, 63549, 64207, 64869, 65535};
// 8-bit easing functions
u8 easeInQuad8(u8 i) {
// Simple quadratic ease-in: i^2 scaled to 8-bit range
// Using scale8(i, i) which computes (i * i) / 255
return scale8(i, i);
}
u8 easeInOutQuad8(u8 i) {
constexpr u16 MAX = 0xFF; // 255
constexpr u16 HALF = (MAX + 1) >> 1; // 128
constexpr u16 DENOM = MAX; // divisor for scaling
constexpr u16 ROUND = DENOM >> 1; // for rounding
if (i < HALF) {
// first half: y = 2·(i/MAX)² → y_i = 2·i² / MAX
u32 t = i;
u32 num = 2 * t * t + ROUND; // 2*i², +half for rounding
return u8(num / DENOM);
} else {
// second half: y = 1 2·(1i/MAX)²
// → y_i = MAX (2·(MAXi)² / MAX)
u32 d = MAX - i;
u32 num = 2 * d * d + ROUND; // 2*(MAXi)², +half for rounding
return u8(MAX - (num / DENOM));
}
}
u8 easeInOutCubic8(u8 i) {
constexpr u16 MAX = 0xFF; // 255
constexpr u16 HALF = (MAX + 1) >> 1; // 128
constexpr u32 DENOM = (u32)MAX * MAX; // 255*255 = 65025
constexpr u32 ROUND = DENOM >> 1; // for rounding
if (i < HALF) {
// first half: y = 4·(i/MAX)³ → y_i = 4·i³ / MAX²
u32 ii = i;
u32 cube = ii * ii * ii; // i³
u32 num = 4 * cube + ROUND; // 4·i³, +half denom for rounding
return u8(num / DENOM);
} else {
// second half: y = 1 ((2·t+2)³)/2
// where t = i/MAX; equivalently:
// y_i = MAX (4·(MAXi)³ / MAX²)
u32 d = MAX - i;
u32 cube = d * d * d; // (MAXi)³
u32 num = 4 * cube + ROUND;
return u8(MAX - (num / DENOM));
}
}
u8 easeOutQuad8(u8 i) {
// ease-out is the inverse of ease-in: 1 - (1-t)²
// For 8-bit: y = MAX - (MAX-i)² / MAX
constexpr u16 MAX = 0xFF;
u32 d = MAX - i; // (MAX - i)
u32 num = d * d + (MAX >> 1); // (MAX-i)² + rounding
return u8(MAX - (num / MAX));
}
u8 easeInCubic8(u8 i) {
// Simple cubic ease-in: i³ scaled to 8-bit range
// y = i³ / MAX²
constexpr u16 MAX = 0xFF;
constexpr u32 DENOM = (u32)MAX * MAX;
constexpr u32 ROUND = DENOM >> 1;
u32 ii = i;
u32 cube = ii * ii * ii; // i³
u32 num = cube + ROUND;
return u8(num / DENOM);
}
u8 easeOutCubic8(u8 i) {
// ease-out cubic: 1 - (1-t)³
// For 8-bit: y = MAX - (MAX-i)³ / MAX²
constexpr u16 MAX = 0xFF;
constexpr u32 DENOM = (u32)MAX * MAX;
constexpr u32 ROUND = DENOM >> 1;
u32 d = MAX - i; // (MAX - i)
u32 cube = d * d * d; // (MAX-i)³
u32 num = cube + ROUND;
return u8(MAX - (num / DENOM));
}
u8 easeInSine8(u8 i) {
static const u8 easeInSineTable[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4,
4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8,
8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 13, 13, 14, 14,
15, 16, 16, 17, 17, 18, 18, 19, 20, 20, 21, 21, 22, 23,
23, 24, 25, 25, 26, 27, 27, 28, 29, 30, 30, 31, 32, 33,
33, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52, 52, 53, 54, 55, 56, 57,
58, 59, 60, 61, 62, 63, 64, 65, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 79, 80, 81, 82, 83, 84, 86, 87, 88,
89, 90, 91, 93, 94, 95, 96, 98, 99, 100, 101, 103, 104, 105,
106, 108, 109, 110, 112, 113, 114, 115, 117, 118, 119, 121, 122, 123,
125, 126, 127, 129, 130, 132, 133, 134, 136, 137, 139, 140, 141, 143,
144, 146, 147, 148, 150, 151, 153, 154, 156, 157, 159, 160, 161, 163,
164, 166, 167, 169, 170, 172, 173, 175, 176, 178, 179, 181, 182, 184,
185, 187, 188, 190, 191, 193, 194, 196, 197, 199, 200, 202, 204, 205,
207, 208, 210, 211, 213, 214, 216, 217, 219, 221, 222, 224, 225, 227,
228, 230, 231, 233, 235, 236, 238, 239, 241, 242, 244, 246, 247, 249,
250, 252, 253, 255};
// ease-in sine: 1 - cos(t * π/2)
// Handle boundary conditions explicitly
return easeInSineTable[i];
}
u8 easeOutSine8(u8 i) {
// ease-out sine: sin(t * π/2)
// Delegate to 16-bit version for consistency and accuracy
// Scale 8-bit input to 16-bit range, call 16-bit function, scale result back
u16 input16 = map8_to_16(i);
u16 result16 = easeOutSine16(input16);
return map16_to_8(result16);
}
u8 easeInOutSine8(u8 i) {
// ease-in-out sine: -(cos(π*t) - 1) / 2
// Delegate to 16-bit version for consistency and accuracy
// Scale 8-bit input to 16-bit range, call 16-bit function, scale result back
u16 input16 = map8_to_16(i);
u16 result16 = easeInOutSine16(input16);
return map16_to_8(result16);
}
// 16-bit easing functions
u16 easeInQuad16(u16 i) {
// Simple quadratic ease-in: i^2 scaled to 16-bit range
// Using scale16(i, i) which computes (i * i) / 65535
return scale16(i, i);
}
u16 easeInOutQuad16(u16 x) {
// 16-bit quadratic ease-in / ease-out function
constexpr u32 MAX = 0xFFFF; // 65535
constexpr u32 HALF = (MAX + 1) >> 1; // 32768
constexpr u32 DENOM = MAX; // divisor
constexpr u32 ROUND = DENOM >> 1; // for rounding
if (x < HALF) {
// first half: y = 2·(x/MAX)² → y_i = 2·x² / MAX
fl::u64 xi = x;
fl::u64 num = 2 * xi * xi + ROUND; // 2*x², +half for rounding
return u16(num / DENOM);
} else {
// second half: y = 1 2·(1x/MAX)² → y_i = MAX (2·(MAXx)² / MAX)
fl::u64 d = MAX - x;
fl::u64 num = 2 * d * d + ROUND; // 2*(MAXx)², +half for rounding
return u16(MAX - (num / DENOM));
}
}
u16 easeInOutCubic16(u16 x) {
const u32 MAX = 0xFFFF; // 65535
const u32 HALF = (MAX + 1) >> 1; // 32768
const fl::u64 M2 = (fl::u64)MAX * MAX; // 65535² = 4 294 836 225
if (x < HALF) {
// first half: y = 4·(x/MAX)³ → y_i = 4·x³ / MAX²
fl::u64 xi = x;
fl::u64 cube = xi * xi * xi; // x³
// add M2/2 for rounding
fl::u64 num = 4 * cube + (M2 >> 1);
return (u16)(num / M2);
} else {
// second half: y = 1 ((2·(1x/MAX))³)/2
// → y_i = MAX (4·(MAXx)³ / MAX²)
fl::u64 d = MAX - x;
fl::u64 cube = d * d * d; // (MAXx)³
fl::u64 num = 4 * cube + (M2 >> 1);
return (u16)(MAX - (num / M2));
}
}
u16 easeOutQuad16(u16 i) {
// ease-out quadratic: 1 - (1-t)²
// For 16-bit: y = MAX - (MAX-i)² / MAX
constexpr u32 MAX = 0xFFFF; // 65535
constexpr u32 ROUND = MAX >> 1; // for rounding
fl::u64 d = MAX - i; // (MAX - i)
fl::u64 num = d * d + ROUND; // (MAX-i)² + rounding
return u16(MAX - (num / MAX));
}
u16 easeInCubic16(u16 i) {
// Simple cubic ease-in: i³ scaled to 16-bit range
// y = i³ / MAX²
constexpr u32 MAX = 0xFFFF; // 65535
constexpr fl::u64 DENOM = (fl::u64)MAX * MAX; // 65535²
constexpr fl::u64 ROUND = DENOM >> 1; // for rounding
fl::u64 ii = i;
fl::u64 cube = ii * ii * ii; // i³
fl::u64 num = cube + ROUND;
return u16(num / DENOM);
}
u16 easeOutCubic16(u16 i) {
// ease-out cubic: 1 - (1-t)³
// For 16-bit: y = MAX - (MAX-i)³ / MAX²
constexpr u32 MAX = 0xFFFF; // 65535
constexpr fl::u64 DENOM = (fl::u64)MAX * MAX; // 65535²
constexpr fl::u64 ROUND = DENOM >> 1; // for rounding
fl::u64 d = MAX - i; // (MAX - i)
fl::u64 cube = d * d * d; // (MAX-i)³
fl::u64 num = cube + ROUND;
return u16(MAX - (num / DENOM));
}
u16 easeInSine16(u16 i) {
// ease-in sine: 1 - cos(t * π/2)
// Handle boundary conditions explicitly
if (i == 0)
return 0;
// Remove the hard-coded boundary for 65535 and let math handle it
// For 16-bit: use cos32 for efficiency and accuracy
// Map i from [0,65535] to [0,4194304] in cos32 space (zero to quarter wave)
// Formula: 1 - cos(t * π/2) where t goes from 0 to 1
// sin32/cos32 quarter cycle is 16777216/4 = 4194304
u32 angle = ((fl::u64)i * 4194304ULL) / 65535ULL;
i32 cos_result = fl::cos32(angle);
// Convert cos32 output and apply easing formula: 1 - cos(t * π/2)
// cos32 output range is [-2147418112, 2147418112]
// At t=0: cos(0) = 2147418112, result should be 0
// At t=1: cos(π/2) = 0, result should be 65535
const fl::i64 MAX_COS32 = 2147418112LL;
// Calculate: (MAX_COS32 - cos_result) and scale to [0, 65535]
fl::i64 adjusted = MAX_COS32 - (fl::i64)cos_result;
// Scale from [0, 2147418112] to [0, 65535]
fl::u64 result = (fl::u64)adjusted * 65535ULL + (MAX_COS32 >> 1); // Add half for rounding
u16 final_result = (u16)(result / (fl::u64)MAX_COS32);
return final_result;
}
u16 easeOutSine16(u16 i) {
// ease-out sine: sin(t * π/2)
// Handle boundary conditions explicitly
if (i == 0)
return 0;
if (i == 65535)
return 65535;
// For 16-bit: use sin32 for efficiency and accuracy
// Map i from [0,65535] to [0,4194304] in sin32 space (zero to quarter wave)
// Formula: sin(t * π/2) where t goes from 0 to 1
// sin32 quarter cycle is 16777216/4 = 4194304
u32 angle = ((fl::u64)i * 4194304ULL) / 65535ULL;
i32 sin_result = fl::sin32(angle);
// Convert sin32 output range [-2147418112, 2147418112] to [0, 65535]
// sin32 output is in range -32767*65536 to +32767*65536
// For ease-out sine, we only use positive portion [0, 2147418112] -> [0, 65535]
return (u16)((fl::u64)sin_result * 65535ULL / 2147418112ULL);
}
u16 easeInOutSine16(u16 i) {
// ease-in-out sine: -(cos(π*t) - 1) / 2
// Handle boundary conditions explicitly
if (i == 0)
return 0;
if (i == 65535)
return 65535;
// For 16-bit: use cos32 for efficiency and accuracy
// Map i from [0,65535] to [0,8388608] in cos32 space (0 to half wave)
// Formula: (1 - cos(π*t)) / 2 where t goes from 0 to 1
// sin32/cos32 half cycle is 16777216/2 = 8388608
u32 angle = ((fl::u64)i * 8388608ULL) / 65535ULL;
i32 cos_result = fl::cos32(angle);
// Convert cos32 output and apply easing formula: (1 - cos(π*t)) / 2
// cos32 output range is [-2147418112, 2147418112]
// We want: (2147418112 - cos_result) / 2, then scale to [0, 65535]
fl::i64 adjusted = (2147418112LL - (fl::i64)cos_result) / 2;
return (u16)((fl::u64)adjusted * 65535ULL / 2147418112ULL);
}
} // namespace fl

View File

@@ -0,0 +1,278 @@
#pragma once
/*
This are accurate and tested easing functions.
Note that the easing functions in lib8tion.h are tuned are implemented wrong, such as easeInOutCubic8 and easeInOutCubic16.
Modern platforms are so fast that the extra performance is not needed, but accuracy is important.
*/
#include "fl/stdint.h"
#include "fl/int.h"
#include "fastled_progmem.h"
namespace fl {
// Gamma 2.8 lookup table for 8-bit to 16-bit gamma correction
// Used for converting linear 8-bit values to gamma-corrected 16-bit values
extern const u16 gamma_2_8[256] FL_PROGMEM;
enum EaseType {
EASE_NONE,
EASE_IN_QUAD,
EASE_OUT_QUAD,
EASE_IN_OUT_QUAD,
EASE_IN_CUBIC,
EASE_OUT_CUBIC,
EASE_IN_OUT_CUBIC,
EASE_IN_SINE,
EASE_OUT_SINE,
EASE_IN_OUT_SINE,
};
// 8-bit easing functions
/// 8-bit quadratic ease-in function
/// Takes an input value 0-255 and returns an eased value 0-255
/// The curve starts slow and accelerates (ease-in only)
u8 easeInQuad8(u8 i);
/// 8-bit quadratic ease-out function
/// Takes an input value 0-255 and returns an eased value 0-255
/// The curve starts fast and decelerates (ease-out only)
u8 easeOutQuad8(u8 i);
/// 8-bit quadratic ease-in/ease-out function
/// Takes an input value 0-255 and returns an eased value 0-255
/// The curve starts slow, accelerates in the middle, then slows down again
u8 easeInOutQuad8(u8 i);
/// 8-bit cubic ease-in function
/// Takes an input value 0-255 and returns an eased value 0-255
/// More pronounced acceleration than quadratic
u8 easeInCubic8(u8 i);
/// 8-bit cubic ease-out function
/// Takes an input value 0-255 and returns an eased value 0-255
/// More pronounced deceleration than quadratic
u8 easeOutCubic8(u8 i);
/// 8-bit cubic ease-in/ease-out function
/// Takes an input value 0-255 and returns an eased value 0-255
/// More pronounced easing curve than quadratic
u8 easeInOutCubic8(u8 i);
/// 8-bit sine ease-in function
/// Takes an input value 0-255 and returns an eased value 0-255
/// Smooth sinusoidal acceleration
u8 easeInSine8(u8 i);
/// 8-bit sine ease-out function
/// Takes an input value 0-255 and returns an eased value 0-255
/// Smooth sinusoidal deceleration
u8 easeOutSine8(u8 i);
/// 8-bit sine ease-in/ease-out function
/// Takes an input value 0-255 and returns an eased value 0-255
/// Smooth sinusoidal acceleration and deceleration
u8 easeInOutSine8(u8 i);
// 16-bit easing functions
/// 16-bit quadratic ease-in function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeInQuad16(u16 i);
/// 16-bit quadratic ease-out function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeOutQuad16(u16 i);
/// 16-bit quadratic ease-in/ease-out function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeInOutQuad16(u16 i);
/// 16-bit cubic ease-in function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeInCubic16(u16 i);
/// 16-bit cubic ease-out function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeOutCubic16(u16 i);
/// 16-bit cubic ease-in/ease-out function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeInOutCubic16(u16 i);
/// 16-bit sine ease-in function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeInSine16(u16 i);
/// 16-bit sine ease-out function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeOutSine16(u16 i);
/// 16-bit sine ease-in/ease-out function
/// Takes an input value 0-65535 and returns an eased value 0-65535
u16 easeInOutSine16(u16 i);
u16 ease16(EaseType type, u16 i);
void ease16(EaseType type, u16* src, u16* dst, u16 count);
u8 ease8(EaseType type, u8 i);
void ease8(EaseType type, u8* src, u8* dst, u8 count);
//////// INLINE FUNCTIONS ////////
inline u16 ease16(EaseType type, u16 i) {
switch (type) {
case EASE_NONE: return i;
case EASE_IN_QUAD: return easeInQuad16(i);
case EASE_OUT_QUAD: return easeOutQuad16(i);
case EASE_IN_OUT_QUAD: return easeInOutQuad16(i);
case EASE_IN_CUBIC: return easeInCubic16(i);
case EASE_OUT_CUBIC: return easeOutCubic16(i);
case EASE_IN_OUT_CUBIC: return easeInOutCubic16(i);
case EASE_IN_SINE: return easeInSine16(i);
case EASE_OUT_SINE: return easeOutSine16(i);
case EASE_IN_OUT_SINE: return easeInOutSine16(i);
default: return i;
}
}
inline void ease16(EaseType type, u16* src, u16* dst, u16 count) {
switch (type) {
case EASE_NONE: return;
case EASE_IN_QUAD: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeInQuad16(src[i]);
}
break;
}
case EASE_OUT_QUAD: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeOutQuad16(src[i]);
}
break;
}
case EASE_IN_OUT_QUAD: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeInOutQuad16(src[i]);
}
break;
}
case EASE_IN_CUBIC: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeInCubic16(src[i]);
}
break;
}
case EASE_OUT_CUBIC: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeOutCubic16(src[i]);
}
break;
}
case EASE_IN_OUT_CUBIC: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeInOutCubic16(src[i]);
}
break;
}
case EASE_IN_SINE: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeInSine16(src[i]);
}
break;
}
case EASE_OUT_SINE: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeOutSine16(src[i]);
}
break;
}
case EASE_IN_OUT_SINE: {
for (u16 i = 0; i < count; i++) {
dst[i] = easeInOutSine16(src[i]);
}
break;
}
}
}
inline u8 ease8(EaseType type, u8 i) {
switch (type) {
case EASE_NONE: return i;
case EASE_IN_QUAD: return easeInQuad8(i);
case EASE_OUT_QUAD: return easeOutQuad8(i);
case EASE_IN_OUT_QUAD: return easeInOutQuad8(i);
case EASE_IN_CUBIC: return easeInCubic8(i);
case EASE_OUT_CUBIC: return easeOutCubic8(i);
case EASE_IN_OUT_CUBIC: return easeInOutCubic8(i);
case EASE_IN_SINE: return easeInSine8(i);
case EASE_OUT_SINE: return easeOutSine8(i);
case EASE_IN_OUT_SINE: return easeInOutSine8(i);
default: return i;
}
}
inline void ease8(EaseType type, u8* src, u8* dst, u8 count) {
switch (type) {
case EASE_NONE: return;
case EASE_IN_QUAD: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeInQuad8(src[i]);
}
break;
}
case EASE_OUT_QUAD: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeOutQuad8(src[i]);
}
break;
}
case EASE_IN_OUT_QUAD: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeInOutQuad8(src[i]);
}
break;
}
case EASE_IN_CUBIC: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeInCubic8(src[i]);
}
break;
}
case EASE_OUT_CUBIC: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeOutCubic8(src[i]);
}
break;
}
case EASE_IN_OUT_CUBIC: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeInOutCubic8(src[i]);
}
break;
}
case EASE_IN_SINE: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeInSine8(src[i]);
}
break;
}
case EASE_OUT_SINE: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeOutSine8(src[i]);
}
break;
}
case EASE_IN_OUT_SINE: {
for (u8 i = 0; i < count; i++) {
dst[i] = easeInOutSine8(src[i]);
}
break;
}
}
}
} // namespace fl

View File

@@ -0,0 +1,131 @@
#include "fl/engine_events.h"
#include "fl/namespace.h"
#include "fl/int.h"
namespace fl {
EngineEvents::Listener::Listener() {}
EngineEvents::Listener::~Listener() {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents *ptr = EngineEvents::getInstance();
const bool has_listener = ptr && ptr->_hasListener(this);
if (has_listener) {
// Warning, the listener should be removed by the subclass. If we are
// here then the subclass did not remove the listener and we are now in
// a partial state of destruction and the results may be undefined for
// multithreaded applications. However, for single threaded (the only
// option as of 2024, October) this will be ok.
ptr->_removeListener(this);
}
#endif
}
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents *EngineEvents::getInstance() {
return &Singleton<EngineEvents>::instance();
}
#endif
#if FASTLED_HAS_ENGINE_EVENTS
void EngineEvents::_onPlatformPreLoop() {
for (auto &item : mListeners) {
auto listener = item.listener;
listener->onPlatformPreLoop();
}
for (auto &item : mListeners) {
auto listener = item.listener;
listener->onPlatformPreLoop2();
}
}
bool EngineEvents::_hasListener(Listener *listener) {
auto predicate = [listener](const Pair &pair) {
return pair.listener == listener;
};
return mListeners.find_if(predicate) != mListeners.end();
}
void EngineEvents::_addListener(Listener *listener, int priority) {
if (_hasListener(listener)) {
return;
}
for (auto it = mListeners.begin(); it != mListeners.end(); ++it) {
if (it->priority < priority) {
// this is now the highest priority in this spot.
EngineEvents::Pair pair = EngineEvents::Pair(listener, priority);
mListeners.insert(it, pair);
return;
}
}
EngineEvents::Pair pair = EngineEvents::Pair(listener, priority);
mListeners.push_back(pair);
}
void EngineEvents::_removeListener(Listener *listener) {
auto predicate = [listener](const Pair &pair) {
return pair.listener == listener;
};
auto it = mListeners.find_if(predicate);
if (it != mListeners.end()) {
mListeners.erase(it);
}
}
void EngineEvents::_onBeginFrame() {
// Make the copy of the listener list to avoid issues with listeners being
// added or removed during the loop.
ListenerList copy = mListeners;
for (auto &item : copy) {
auto listener = item.listener;
listener->onBeginFrame();
}
}
void EngineEvents::_onEndShowLeds() {
// Make the copy of the listener list to avoid issues with listeners being
// added or removed during the loop.
ListenerList copy = mListeners;
for (auto &item : copy) {
auto listener = item.listener;
listener->onEndShowLeds();
}
}
void EngineEvents::_onEndFrame() {
// Make the copy of the listener list to avoid issues with listeners being
// added or removed during the loop.
ListenerList copy = mListeners;
for (auto &item : copy) {
auto listener = item.listener;
listener->onEndFrame();
}
}
void EngineEvents::_onStripAdded(CLEDController *strip, fl::u32 num_leds) {
// Make the copy of the listener list to avoid issues with listeners being
// added or removed during the loop.
ListenerList copy = mListeners;
for (auto &item : copy) {
auto listener = item.listener;
listener->onStripAdded(strip, num_leds);
}
}
void EngineEvents::_onCanvasUiSet(CLEDController *strip,
const ScreenMap &screenmap) {
// Make the copy of the listener list to avoid issues with listeners being
// added or removed during the loop.
ListenerList copy = mListeners;
for (auto &item : copy) {
auto listener = item.listener;
listener->onCanvasUiSet(strip, screenmap);
}
}
#endif // FASTLED_HAS_ENGINE_EVENTS
} // namespace fl

View File

@@ -0,0 +1,150 @@
#pragma once
#include "fl/namespace.h"
#include "fl/screenmap.h"
#include "fl/singleton.h"
#include "fl/vector.h"
#include "fl/xymap.h"
#include "fl/string.h"
#include "fl/int.h"
#ifndef FASTLED_HAS_ENGINE_EVENTS
#define FASTLED_HAS_ENGINE_EVENTS SKETCH_HAS_LOTS_OF_MEMORY
#endif // FASTLED_HAS_ENGINE_EVENTS
FASTLED_NAMESPACE_BEGIN
class CLEDController;
FASTLED_NAMESPACE_END
namespace fl {
class EngineEvents {
public:
class Listener {
public:
// Note that the subclass must call EngineEvents::addListener(this) to
// start listening. In the subclass destructor, the subclass should call
// EngineEvents::removeListener(this).
Listener();
virtual ~Listener();
virtual void onBeginFrame() {}
virtual void onEndShowLeds() {}
virtual void onEndFrame() {}
virtual void onStripAdded(CLEDController *strip, fl::u32 num_leds) {
(void)strip;
(void)num_leds;
}
// Called to set the canvas for UI elements for a particular strip.
virtual void onCanvasUiSet(CLEDController *strip,
const ScreenMap &screenmap) {
(void)strip;
(void)screenmap;
}
virtual void onPlatformPreLoop() {}
virtual void onPlatformPreLoop2() {}
};
static void addListener(Listener *listener, int priority = 0) {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_addListener(listener, priority);
#else
(void)listener;
(void)priority;
#endif
}
static void removeListener(Listener *listener) {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_removeListener(listener);
#else
(void)listener;
#endif
}
static bool hasListener(Listener *listener) {
#if FASTLED_HAS_ENGINE_EVENTS
return EngineEvents::getInstance()->_hasListener(listener);
#else
(void)listener;
return false;
#endif
}
static void onBeginFrame() {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_onBeginFrame();
#endif
}
static void onEndShowLeds() {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_onEndShowLeds();
#endif
}
static void onEndFrame() {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_onEndFrame();
#endif
}
static void onStripAdded(CLEDController *strip, fl::u32 num_leds) {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_onStripAdded(strip, num_leds);
#else
(void)strip;
(void)num_leds;
#endif
}
static void onCanvasUiSet(CLEDController *strip, const ScreenMap &xymap) {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_onCanvasUiSet(strip, xymap);
#else
(void)strip;
(void)xymap;
#endif
}
static void onPlatformPreLoop() {
#if FASTLED_HAS_ENGINE_EVENTS
EngineEvents::getInstance()->_onPlatformPreLoop();
#endif
}
// Needed by fl::vector<T>
EngineEvents() = default;
private:
// Safe to add a listeners during a callback.
void _addListener(Listener *listener, int priority);
// Safe to remove self during a callback.
void _removeListener(Listener *listener);
void _onBeginFrame();
void _onEndShowLeds();
void _onEndFrame();
void _onStripAdded(CLEDController *strip, fl::u32 num_leds);
void _onCanvasUiSet(CLEDController *strip, const ScreenMap &xymap);
void _onPlatformPreLoop();
bool _hasListener(Listener *listener);
#if FASTLED_HAS_ENGINE_EVENTS
struct Pair {
Pair() = default;
Listener *listener = nullptr;
int priority = 0;
Pair(Listener *listener, int priority)
: listener(listener), priority(priority) {}
};
typedef fl::vector_inlined<Pair, 16> ListenerList;
ListenerList mListeners;
static EngineEvents *getInstance();
friend class fl::Singleton<EngineEvents>;
#endif // FASTLED_HAS_ENGINE_EVENTS
};
} // namespace fl

View File

@@ -0,0 +1,31 @@
/// @file fl/eorder.h
/// Defines color channel ordering enumerations in the fl namespace
#pragma once
namespace fl {
/// RGB color channel orderings, used when instantiating controllers to determine
/// what order the controller should send data out in. The default ordering
/// is RGB.
/// Within this enum, the red channel is 0, the green channel is 1, and the
/// blue chanel is 2.
enum EOrder {
RGB=0012, ///< Red, Green, Blue (0012)
RBG=0021, ///< Red, Blue, Green (0021)
GRB=0102, ///< Green, Red, Blue (0102)
GBR=0120, ///< Green, Blue, Red (0120)
BRG=0201, ///< Blue, Red, Green (0201)
BGR=0210 ///< Blue, Green, Red (0210)
};
// After EOrder is applied this is where W is inserted for RGBW.
enum EOrderW {
W3 = 0x3, ///< White is fourth
W2 = 0x2, ///< White is third
W1 = 0x1, ///< White is second
W0 = 0x0, ///< White is first
WDefault = W3
};
} // namespace fl

View File

@@ -0,0 +1,40 @@
#pragma once
/// @file export.h
/// Cross-platform export macros for FastLED dynamic library support
#ifndef FASTLED_EXPORT
#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#define FASTLED_EXPORT EMSCRIPTEN_KEEPALIVE
#elif defined(_WIN32) || defined(_WIN64)
// Windows DLL export/import
#ifdef FASTLED_BUILDING_DLL
#define FASTLED_EXPORT __declspec(dllexport)
#else
#define FASTLED_EXPORT __declspec(dllimport)
#endif
#elif defined(__GNUC__) && (__GNUC__ >= 4)
// GCC/Clang visibility attributes
#define FASTLED_EXPORT __attribute__((visibility("default")))
#elif defined(__SUNPRO_CC) && (__SUNPRO_CC >= 0x550)
// Sun Studio visibility attributes
#define FASTLED_EXPORT __global
#else
// Fallback for other platforms
#define FASTLED_EXPORT
#endif
#endif
#ifndef FASTLED_CALL
#if defined(_WIN32) || defined(_WIN64)
// Windows calling convention
#define FASTLED_CALL __stdcall
#else
// Unix-like platforms - no special calling convention
#define FASTLED_CALL
#endif
#endif
/// Combined export and calling convention macro
#define FASTLED_API FASTLED_EXPORT FASTLED_CALL

View File

@@ -0,0 +1,333 @@
#include "fl/fetch.h"
#include "fl/warn.h"
#include "fl/str.h"
#include "fl/mutex.h"
#include "fl/singleton.h"
#include "fl/engine_events.h"
#include "fl/async.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/val.h>
#endif
// Include WASM-specific implementation
#include "platforms/wasm/js_fetch.h"
namespace fl {
#ifdef __EMSCRIPTEN__
// ========== WASM Implementation using JavaScript fetch ==========
// Promise storage moved to FetchManager singleton
// Use existing WASM fetch infrastructure
void fetch(const fl::string& url, const FetchCallback& callback) {
// Use the existing WASM fetch implementation - no conversion needed since both use fl::response
wasm_fetch.get(url).response(callback);
}
// Internal helper to execute a fetch request and return a promise
fl::promise<response> execute_fetch_request(const fl::string& url, const fetch_options& request) {
// Create a promise for this request
auto promise = fl::promise<response>::create();
// Register with fetch manager to ensure it's tracked
FetchManager::instance().register_promise(promise);
// Get the actual URL to use (use request URL if provided, otherwise use parameter URL)
fl::string fetch_url = request.url().empty() ? url : request.url();
// Convert our request to the existing WASM fetch system
auto wasm_request = WasmFetchRequest(fetch_url);
// Use lambda that captures the promise directly (shared_ptr is safe to copy)
// Make the lambda mutable so we can call non-const methods on the captured promise
wasm_request.response([promise](const response& resp) mutable {
// Complete the promise directly - no need for double storage
if (promise.valid()) {
promise.complete_with_value(resp);
}
});
return promise;
}
#else
// ========== Embedded/Stub Implementation ==========
void fetch(const fl::string& url, const FetchCallback& callback) {
(void)url; // Unused in stub implementation
// For embedded platforms, immediately call callback with a "not supported" response
response resp(501, "Not Implemented");
resp.set_text("HTTP fetch not supported on this platform");
callback(resp);
}
// Internal helper to execute a fetch request and return a promise
fl::promise<response> execute_fetch_request(const fl::string& url, const fetch_options& request) {
(void)request; // Unused in stub implementation
FL_WARN("HTTP fetch is not supported on non-WASM platforms. URL: " << url);
// Create error response
response error_response(501, "Not Implemented");
error_response.set_body("HTTP fetch is only available in WASM/browser builds. This platform does not support network requests.");
// Create resolved promise with error response
auto promise = fl::promise<response>::resolve(error_response);
return promise;
}
#endif
// ========== Engine Events Integration ==========
// ========== Promise-Based API Implementation ==========
class FetchEngineListener : public EngineEvents::Listener {
public:
FetchEngineListener() {
EngineEvents::addListener(this);
};
~FetchEngineListener() override {
// Listener base class automatically removes itself
EngineEvents::removeListener(this);
}
void onEndFrame() override {
// Update all async tasks (fetch, timers, etc.) at the end of each frame
fl::async_run();
}
};
FetchManager& FetchManager::instance() {
return fl::Singleton<FetchManager>::instance();
}
void FetchManager::register_promise(const fl::promise<response>& promise) {
// Auto-register with async system and engine listener on first promise
if (mActivePromises.empty()) {
AsyncManager::instance().register_runner(this);
if (!mEngineListener) {
mEngineListener = fl::make_unique<FetchEngineListener>();
EngineEvents::addListener(mEngineListener.get());
}
}
mActivePromises.push_back(promise);
}
void FetchManager::update() {
// Update all active promises first
for (auto& promise : mActivePromises) {
if (promise.valid()) {
promise.update();
}
}
// Then clean up completed/invalid promises in a separate pass
cleanup_completed_promises();
// Auto-unregister from async system when no more promises
if (mActivePromises.empty()) {
AsyncManager::instance().unregister_runner(this);
if (mEngineListener) {
EngineEvents::removeListener(mEngineListener.get());
mEngineListener.reset();
}
}
}
bool FetchManager::has_active_tasks() const {
return !mActivePromises.empty();
}
size_t FetchManager::active_task_count() const {
return mActivePromises.size();
}
fl::size FetchManager::active_requests() const {
return mActivePromises.size();
}
void FetchManager::cleanup_completed_promises() {
// Rebuild vector without completed promises
fl::vector<fl::promise<response>> active_promises;
for (const auto& promise : mActivePromises) {
if (promise.valid() && !promise.is_completed()) {
active_promises.push_back(promise);
}
}
mActivePromises = fl::move(active_promises);
}
// WASM promise management methods removed - no longer needed
// Promises are now handled directly via shared_ptr capture in callbacks
// ========== Public API Functions ==========
fl::promise<response> fetch_get(const fl::string& url, const fetch_options& request) {
// Create a new request with GET method
fetch_options get_request(url, RequestOptions("GET"));
// Apply any additional options from the provided request
const auto& opts = request.options();
get_request.timeout(opts.timeout_ms);
for (const auto& header : opts.headers) {
get_request.header(header.first, header.second);
}
if (!opts.body.empty()) {
get_request.body(opts.body);
}
return execute_fetch_request(url, get_request);
}
fl::promise<response> fetch_post(const fl::string& url, const fetch_options& request) {
// Create a new request with POST method
fetch_options post_request(url, RequestOptions("POST"));
// Apply any additional options from the provided request
const auto& opts = request.options();
post_request.timeout(opts.timeout_ms);
for (const auto& header : opts.headers) {
post_request.header(header.first, header.second);
}
if (!opts.body.empty()) {
post_request.body(opts.body);
}
return execute_fetch_request(url, post_request);
}
fl::promise<response> fetch_put(const fl::string& url, const fetch_options& request) {
// Create a new request with PUT method
fetch_options put_request(url, RequestOptions("PUT"));
// Apply any additional options from the provided request
const auto& opts = request.options();
put_request.timeout(opts.timeout_ms);
for (const auto& header : opts.headers) {
put_request.header(header.first, header.second);
}
if (!opts.body.empty()) {
put_request.body(opts.body);
}
return execute_fetch_request(url, put_request);
}
fl::promise<response> fetch_delete(const fl::string& url, const fetch_options& request) {
// Create a new request with DELETE method
fetch_options delete_request(url, RequestOptions("DELETE"));
// Apply any additional options from the provided request
const auto& opts = request.options();
delete_request.timeout(opts.timeout_ms);
for (const auto& header : opts.headers) {
delete_request.header(header.first, header.second);
}
if (!opts.body.empty()) {
delete_request.body(opts.body);
}
return execute_fetch_request(url, delete_request);
}
fl::promise<response> fetch_head(const fl::string& url, const fetch_options& request) {
// Create a new request with HEAD method
fetch_options head_request(url, RequestOptions("HEAD"));
// Apply any additional options from the provided request
const auto& opts = request.options();
head_request.timeout(opts.timeout_ms);
for (const auto& header : opts.headers) {
head_request.header(header.first, header.second);
}
if (!opts.body.empty()) {
head_request.body(opts.body);
}
return execute_fetch_request(url, head_request);
}
fl::promise<response> fetch_http_options(const fl::string& url, const fetch_options& request) {
// Create a new request with OPTIONS method
fetch_options options_request(url, RequestOptions("OPTIONS"));
// Apply any additional options from the provided request
const auto& opts = request.options();
options_request.timeout(opts.timeout_ms);
for (const auto& header : opts.headers) {
options_request.header(header.first, header.second);
}
if (!opts.body.empty()) {
options_request.body(opts.body);
}
return execute_fetch_request(url, options_request);
}
fl::promise<response> fetch_patch(const fl::string& url, const fetch_options& request) {
// Create a new request with PATCH method
fetch_options patch_request(url, RequestOptions("PATCH"));
// Apply any additional options from the provided request
const auto& opts = request.options();
patch_request.timeout(opts.timeout_ms);
for (const auto& header : opts.headers) {
patch_request.header(header.first, header.second);
}
if (!opts.body.empty()) {
patch_request.body(opts.body);
}
return execute_fetch_request(url, patch_request);
}
fl::promise<response> fetch_request(const fl::string& url, const RequestOptions& options) {
// Create a fetch_options with the provided options
fetch_options request(url, options);
// Use the helper function to execute the request
return execute_fetch_request(url, request);
}
void fetch_update() {
// Legacy function - use fl::async_run() for new code
// This provides backwards compatibility for existing code
fl::async_run();
}
fl::size fetch_active_requests() {
return FetchManager::instance().active_requests();
}
// ========== Response Class Method Implementations ==========
fl::Json response::json() const {
if (!mJsonParsed) {
if (is_json() || mBody.find("{") != fl::string::npos || mBody.find("[") != fl::string::npos) {
mCachedJson = parse_json_body();
} else {
FL_WARN("Response is not JSON: " << mBody);
mCachedJson = fl::Json(nullptr); // Not JSON content
}
mJsonParsed = true;
}
return mCachedJson.has_value() ? *mCachedJson : fl::Json(nullptr);
}
} // namespace fl

View File

@@ -0,0 +1,326 @@
#pragma once
/// @file fetch.h
/// @brief Unified HTTP fetch API for FastLED (cross-platform)
///
/// This API provides both simple callback-based and JavaScript-like promise-based interfaces
/// for HTTP requests. Works on WASM/browser platforms with real fetch, provides stubs on embedded.
///
/// **WASM Optimization:** On WASM platforms, `delay()` automatically pumps all async tasks
/// (fetch, timers, etc.) in 1ms intervals, making delay time useful for processing async operations.
///
/// @section Simple Callback Usage
/// @code
/// #include "fl/fetch.h"
///
/// void setup() {
/// // Simple callback-based fetch (backward compatible)
/// fl::fetch("http://fastled.io", [](const fl::response& resp) {
/// if (resp.ok()) {
/// FL_WARN("Success: " << resp.text());
/// }
/// });
/// }
/// @endcode
///
/// @section Promise Usage
/// @code
/// #include "fl/fetch.h"
///
/// void setup() {
/// // JavaScript-like fetch with promises
/// fl::fetch_get("http://fastled.io")
/// .then([](const fl::response& resp) {
/// if (resp.ok()) {
/// FL_WARN("Success: " << resp.text());
/// } else {
/// FL_WARN("HTTP Error: " << resp.status() << " " << resp.status_text());
/// }
/// })
/// .catch_([](const fl::Error& err) {
/// FL_WARN("Fetch Error: " << err.message);
/// });
/// }
///
/// void loop() {
/// // Fetch promises are automatically updated through FastLED's engine events!
/// // On WASM platforms, delay() also pumps all async tasks automatically.
/// // No manual updates needed - just use normal FastLED loop
/// FastLED.show();
/// delay(16); // delay() automatically pumps all async tasks on WASM
/// }
/// @endcode
#include "fl/namespace.h"
#include "fl/promise.h"
#include "fl/string.h"
#include "fl/vector.h"
#include "fl/map.h"
#include "fl/hash_map.h"
#include "fl/optional.h"
#include "fl/function.h"
#include "fl/ptr.h"
#include "fl/async.h"
#include "fl/mutex.h"
#include "fl/warn.h"
#include "fl/json.h" // Add JSON support for response.json() method
namespace fl {
// Forward declarations
class fetch_options;
class FetchManager;
#ifdef __EMSCRIPTEN__
// Forward declarations for WASM-specific types (defined in platforms/wasm/js_fetch.h)
class WasmFetchRequest;
class WasmFetch;
using FetchResponseCallback = fl::function<void(const response&)>;
extern WasmFetch wasm_fetch;
#endif
/// HTTP response class (unified interface)
class response {
public:
response() : mStatusCode(200), mStatusText("OK") {}
response(int status_code) : mStatusCode(status_code), mStatusText(get_default_status_text(status_code)) {}
response(int status_code, const fl::string& status_text)
: mStatusCode(status_code), mStatusText(status_text) {}
/// HTTP status code (like JavaScript response.status)
int status() const { return mStatusCode; }
/// HTTP status text (like JavaScript response.statusText)
const fl::string& status_text() const { return mStatusText; }
/// Check if response is successful (like JavaScript response.ok)
bool ok() const { return mStatusCode >= 200 && mStatusCode < 300; }
/// Response body as text (like JavaScript response.text())
const fl::string& text() const { return mBody; }
/// Get header value (like JavaScript response.headers.get())
fl::optional<fl::string> get_header(const fl::string& name) const {
auto it = mHeaders.find(name);
if (it != mHeaders.end()) {
return fl::make_optional(it->second);
}
return fl::nullopt;
}
/// Get content type convenience method
fl::optional<fl::string> get_content_type() const {
return get_header("content-type");
}
/// Response body as text (alternative to text())
const fl::string& get_body_text() const { return mBody; }
/// Response body parsed as JSON (JavaScript-like API)
/// @return fl::Json object for safe, ergonomic access
/// @note Automatically parses JSON on first call, caches result
/// @note Returns null JSON object for non-JSON or malformed content
fl::Json json() const;
/// Check if response appears to contain JSON content
/// @return true if Content-Type header indicates JSON or body contains JSON markers
bool is_json() const {
auto content_type = get_content_type();
if (content_type.has_value()) {
fl::string ct = *content_type;
// Check for various JSON content types (case-insensitive)
return ct.find("json") != fl::string::npos;
}
return false;
}
/// Set methods (internal use)
void set_status(int status_code) { mStatusCode = status_code; }
void set_status_text(const fl::string& status_text) { mStatusText = status_text; }
void set_text(const fl::string& body) { mBody = body; } // Backward compatibility
void set_body(const fl::string& body) { mBody = body; }
void set_header(const fl::string& name, const fl::string& value) {
mHeaders[name] = value;
}
private:
int mStatusCode;
fl::string mStatusText;
fl::string mBody;
fl_map<fl::string, fl::string> mHeaders;
// JSON parsing cache
mutable fl::optional<fl::Json> mCachedJson; // Lazy-loaded JSON cache
mutable bool mJsonParsed = false; // Track parsing attempts
/// Parse JSON from response body with error handling
fl::Json parse_json_body() const {
fl::Json parsed = fl::Json::parse(mBody);
if (parsed.is_null() && (!mBody.empty())) {
// If parsing failed but we have content, return null JSON
// This allows safe chaining: resp.json()["key"] | default
return fl::Json(nullptr);
}
return parsed;
}
static fl::string get_default_status_text(int status) {
switch (status) {
case 200: return "OK";
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 403: return "Forbidden";
case 404: return "Not Found";
case 500: return "Internal Server Error";
case 501: return "Not Implemented";
case 502: return "Bad Gateway";
case 503: return "Service Unavailable";
default: return "Unknown";
}
}
};
/// Callback type for simple fetch responses (backward compatible)
using FetchCallback = fl::function<void(const response&)>;
/// Request options (matches JavaScript fetch RequestInit)
struct RequestOptions {
fl::string method = "GET";
fl_map<fl::string, fl::string> headers;
fl::string body;
int timeout_ms = 10000; // 10 second default
RequestOptions() = default;
RequestOptions(const fl::string& method_name) : method(method_name) {}
};
/// Fetch options builder (fluent interface)
class fetch_options {
public:
explicit fetch_options(const fl::string& url) : mUrl(url) {}
fetch_options(const fl::string& url, const RequestOptions& options)
: mUrl(url), mOptions(options) {}
/// Set HTTP method
fetch_options& method(const fl::string& http_method) {
mOptions.method = http_method;
return *this;
}
/// Add header
fetch_options& header(const fl::string& name, const fl::string& value) {
mOptions.headers[name] = value;
return *this;
}
/// Set request body
fetch_options& body(const fl::string& data) {
mOptions.body = data;
return *this;
}
/// Set JSON body with proper content type
fetch_options& json(const fl::string& json_data) {
mOptions.body = json_data;
mOptions.headers["Content-Type"] = "application/json";
return *this;
}
/// Set timeout in milliseconds
fetch_options& timeout(int timeout_ms) {
mOptions.timeout_ms = timeout_ms;
return *this;
}
/// Get the URL for this request
const fl::string& url() const { return mUrl; }
/// Get the options for this request
const RequestOptions& options() const { return mOptions; }
private:
fl::string mUrl;
RequestOptions mOptions;
friend class FetchManager;
};
class FetchEngineListener;
/// Internal fetch manager for promise tracking
class FetchManager : public async_runner {
public:
static FetchManager& instance();
void register_promise(const fl::promise<response>& promise);
// async_runner interface
void update() override;
bool has_active_tasks() const override;
size_t active_task_count() const override;
// Legacy API
fl::size active_requests() const;
void cleanup_completed_promises();
private:
fl::vector<fl::promise<response>> mActivePromises;
fl::unique_ptr<FetchEngineListener> mEngineListener;
};
// ========== Simple Callback API (Backward Compatible) ==========
/// @brief Make an HTTP GET request (cross-platform, backward compatible)
/// @param url The URL to fetch
/// @param callback Function to call with the response
///
/// On WASM/browser platforms: Uses native JavaScript fetch() API
/// On Arduino/embedded platforms: Immediately calls callback with error response
void fetch(const fl::string& url, const FetchCallback& callback);
/// @brief Make an HTTP GET request with URL string literal (cross-platform)
/// @param url The URL to fetch (C-string)
/// @param callback Function to call with the response
inline void fetch(const char* url, const FetchCallback& callback) {
fetch(fl::string(url), callback);
}
// ========== Promise-Based API (JavaScript-like) ==========
/// HTTP GET request
fl::promise<response> fetch_get(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP POST request
fl::promise<response> fetch_post(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP PUT request
fl::promise<response> fetch_put(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP DELETE request
fl::promise<response> fetch_delete(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP HEAD request
fl::promise<response> fetch_head(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP OPTIONS request
fl::promise<response> fetch_http_options(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP PATCH request
fl::promise<response> fetch_patch(const fl::string& url, const fetch_options& request = fetch_options(""));
/// Generic request with options (like fetch(url, options))
fl::promise<response> fetch_request(const fl::string& url, const RequestOptions& options = RequestOptions());
/// Legacy manual update for fetch promises (use fl::async_run() for new code)
/// @deprecated Use fl::async_run() instead - this calls async_run() internally
void fetch_update();
/// Get number of active requests
fl::size fetch_active_requests();
/// Internal helper to execute a fetch request and return a promise
fl::promise<response> execute_fetch_request(const fl::string& url, const fetch_options& request);
} // namespace fl

View File

@@ -0,0 +1,74 @@
#include "fl/fft.h"
#include "fl/compiler_control.h"
#include "fl/fft_impl.h"
#include "fl/hash_map_lru.h"
#include "fl/int.h"
#include "fl/memory.h"
namespace fl {
template <> struct Hash<FFT_Args> {
fl::u32 operator()(const FFT_Args &key) const noexcept {
return MurmurHash3_x86_32(&key, sizeof(FFT_Args));
}
};
struct FFT::HashMap : public HashMapLru<FFT_Args, fl::shared_ptr<FFTImpl>> {
HashMap(fl::size max_size)
: fl::HashMapLru<FFT_Args, fl::shared_ptr<FFTImpl>>(max_size) {}
};
FFT::FFT() { mMap.reset(new HashMap(8)); };
FFT::~FFT() = default;
FFT::FFT(const FFT &other) {
// copy the map
mMap.reset();
mMap.reset(new HashMap(*other.mMap));
}
FFT &FFT::operator=(const FFT &other) {
mMap.reset();
mMap.reset(new HashMap(*other.mMap));
return *this;
}
void FFT::run(const span<const fl::i16> &sample, FFTBins *out,
const FFT_Args &args) {
FFT_Args args2 = args;
args2.samples = sample.size();
get_or_create(args2).run(sample, out);
}
void FFT::clear() { mMap->clear(); }
fl::size FFT::size() const { return mMap->size(); }
void FFT::setFFTCacheSize(fl::size size) { mMap->setMaxSize(size); }
FFTImpl &FFT::get_or_create(const FFT_Args &args) {
fl::shared_ptr<FFTImpl> *val = mMap->find_value(args);
if (val) {
// we have it.
return **val;
}
// else we have to make a new one.
fl::shared_ptr<FFTImpl> fft = fl::make_shared<FFTImpl>(args);
(*mMap)[args] = fft;
return *fft;
}
bool FFT_Args::operator==(const FFT_Args &other) const {
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING(float-equal);
return samples == other.samples && bands == other.bands &&
fmin == other.fmin && fmax == other.fmax &&
sample_rate == other.sample_rate;
FL_DISABLE_WARNING_POP
}
} // namespace fl

View File

@@ -0,0 +1,125 @@
#pragma once
#include "fl/unique_ptr.h"
#include "fl/span.h"
#include "fl/vector.h"
#include "fl/move.h"
#include "fl/memfill.h"
namespace fl {
class FFTImpl;
class AudioSample;
struct FFTBins {
public:
FFTBins(fl::size n) : mSize(n) {
bins_raw.reserve(n);
bins_db.reserve(n);
}
// Copy constructor and assignment
FFTBins(const FFTBins &other) : bins_raw(other.bins_raw), bins_db(other.bins_db), mSize(other.mSize) {}
FFTBins &operator=(const FFTBins &other) {
if (this != &other) {
mSize = other.mSize;
bins_raw = other.bins_raw;
bins_db = other.bins_db;
}
return *this;
}
// Move constructor and assignment
FFTBins(FFTBins &&other) noexcept
: bins_raw(fl::move(other.bins_raw)), bins_db(fl::move(other.bins_db)), mSize(other.mSize) {}
FFTBins &operator=(FFTBins &&other) noexcept {
if (this != &other) {
bins_raw = fl::move(other.bins_raw);
bins_db = fl::move(other.bins_db);
mSize = other.mSize;
}
return *this;
}
void clear() {
bins_raw.clear();
bins_db.clear();
}
fl::size size() const { return mSize; }
// The bins are the output of the FFTImpl.
fl::vector<float> bins_raw;
// The frequency range of the bins.
fl::vector<float> bins_db;
private:
fl::size mSize;
};
struct FFT_Args {
static int DefaultSamples() { return 512; }
static int DefaultBands() { return 16; }
static float DefaultMinFrequency() { return 174.6f; }
static float DefaultMaxFrequency() { return 4698.3f; }
static int DefaultSampleRate() { return 44100; }
int samples;
int bands;
float fmin;
float fmax;
int sample_rate;
FFT_Args(int samples = DefaultSamples(), int bands = DefaultBands(),
float fmin = DefaultMinFrequency(),
float fmax = DefaultMaxFrequency(),
int sample_rate = DefaultSampleRate()) {
// Memset so that this object can be hashed without garbage from packed
// in data.
fl::memfill(this, 0, sizeof(FFT_Args));
this->samples = samples;
this->bands = bands;
this->fmin = fmin;
this->fmax = fmax;
this->sample_rate = sample_rate;
}
// Rule of 5 for POD data
FFT_Args(const FFT_Args &other) = default;
FFT_Args &operator=(const FFT_Args &other) = default;
FFT_Args(FFT_Args &&other) noexcept = default;
FFT_Args &operator=(FFT_Args &&other) noexcept = default;
bool operator==(const FFT_Args &other) const ;
bool operator!=(const FFT_Args &other) const { return !(*this == other); }
};
class FFT {
public:
FFT();
~FFT();
FFT(FFT &&) = default;
FFT &operator=(FFT &&) = default;
FFT(const FFT & other);
FFT &operator=(const FFT & other);
void run(const span<const i16> &sample, FFTBins *out,
const FFT_Args &args = FFT_Args());
void clear();
fl::size size() const;
// FFT's are expensive to create, so we cache them. This sets the size of
// the cache. The default is 8.
void setFFTCacheSize(fl::size size);
private:
// Get the FFTImpl for the given arguments.
FFTImpl &get_or_create(const FFT_Args &args);
struct HashMap;
scoped_ptr<HashMap> mMap;
};
}; // namespace fl

View File

@@ -0,0 +1,171 @@
/// #include <Arduino.h>
// #include <iostream>
// #include "audio_types.h"
// // #include "defs.h"
// #include "thirdparty/cq_kernel/cq_kernel.h"
// #include "thirdparty/cq_kernel/kiss_fftr.h"
// #include "util.h"
#ifndef FASTLED_INTERNAL
#define FASTLED_INTERNAL 1
#endif
#include "FastLED.h"
#include "third_party/cq_kernel/cq_kernel.h"
#include "third_party/cq_kernel/kiss_fftr.h"
#include "fl/array.h"
#include "fl/audio.h"
#include "fl/fft.h"
#include "fl/fft_impl.h"
#include "fl/str.h"
#include "fl/unused.h"
#include "fl/vector.h"
#include "fl/warn.h"
#include "fl/memfill.h"
#define AUDIO_SAMPLE_RATE 44100
#define SAMPLES 512
#define BANDS 16
#define SAMPLING_FREQUENCY AUDIO_SAMPLE_RATE
#define MAX_FREQUENCY 4698.3
#define MIN_FREQUENCY 174.6
#define MIN_VAL 5000 // Equivalent to 0.15 in Q15
#define PRINT_HEADER 1
namespace fl {
class FFTContext {
public:
FFTContext(int samples, int bands, float fmin, float fmax, int sample_rate)
: m_fftr_cfg(nullptr), m_kernels(nullptr) {
fl::memfill(&m_cq_cfg, 0, sizeof(m_cq_cfg));
m_cq_cfg.samples = samples;
m_cq_cfg.bands = bands;
m_cq_cfg.fmin = fmin;
m_cq_cfg.fmax = fmax;
m_cq_cfg.fs = sample_rate;
m_cq_cfg.min_val = MIN_VAL;
m_fftr_cfg = kiss_fftr_alloc(samples, 0, NULL, NULL);
if (!m_fftr_cfg) {
FASTLED_WARN("Failed to allocate FFTImpl context");
return;
}
m_kernels = generate_kernels(m_cq_cfg);
}
~FFTContext() {
if (m_fftr_cfg) {
kiss_fftr_free(m_fftr_cfg);
}
if (m_kernels) {
free_kernels(m_kernels, m_cq_cfg);
}
}
fl::size sampleSize() const { return m_cq_cfg.samples; }
void fft_unit_test(span<const i16> buffer, FFTBins *out) {
// FASTLED_ASSERT(512 == m_cq_cfg.samples, "FFTImpl samples mismatch and
// are still hardcoded to 512");
out->clear();
// allocate
FASTLED_STACK_ARRAY(kiss_fft_cpx, fft, m_cq_cfg.samples);
FASTLED_STACK_ARRAY(kiss_fft_cpx, cq, m_cq_cfg.bands);
// initialize
kiss_fftr(m_fftr_cfg, buffer.data(), fft);
apply_kernels(fft, cq, m_kernels, m_cq_cfg);
const float maxf = m_cq_cfg.fmax;
const float minf = m_cq_cfg.fmin;
const float delta_f = (maxf - minf) / m_cq_cfg.bands;
// begin transform
for (int i = 0; i < m_cq_cfg.bands; ++i) {
i32 real = cq[i].r;
i32 imag = cq[i].i;
float r2 = float(real * real);
float i2 = float(imag * imag);
float magnitude = sqrt(r2 + i2);
float magnitude_db = 20 * log10(magnitude);
float f_start = minf + i * delta_f;
float f_end = f_start + delta_f;
FASTLED_UNUSED(f_start);
FASTLED_UNUSED(f_end);
if (magnitude <= 0.0f) {
magnitude_db = 0.0f;
}
// FASTLED_UNUSED(magnitude_db);
// FASTLED_WARN("magnitude_db: " << magnitude_db);
// out->push_back(magnitude_db);
out->bins_raw.push_back(magnitude);
out->bins_db.push_back(magnitude_db);
}
}
fl::string info() const {
// Calculate frequency delta
float delta_f = (m_cq_cfg.fmax - m_cq_cfg.fmin) / m_cq_cfg.bands;
fl::StrStream ss;
ss << "FFTImpl Frequency Bands: ";
for (int i = 0; i < m_cq_cfg.bands; ++i) {
float f_start = m_cq_cfg.fmin + i * delta_f;
float f_end = f_start + delta_f;
ss << f_start << "Hz-" << f_end << "Hz, ";
}
return ss.str();
}
private:
kiss_fftr_cfg m_fftr_cfg;
cq_kernels_t m_kernels;
cq_kernel_cfg m_cq_cfg;
};
FFTImpl::FFTImpl(const FFT_Args &args) {
mContext.reset(new FFTContext(args.samples, args.bands, args.fmin,
args.fmax, args.sample_rate));
}
FFTImpl::~FFTImpl() { mContext.reset(); }
fl::string FFTImpl::info() const {
if (mContext) {
return mContext->info();
} else {
FASTLED_WARN("FFTImpl context is not initialized");
return fl::string();
}
}
fl::size FFTImpl::sampleSize() const {
if (mContext) {
return mContext->sampleSize();
}
return 0;
}
FFTImpl::Result FFTImpl::run(const AudioSample &sample, FFTBins *out) {
auto &audio_sample = sample.pcm();
span<const i16> slice(audio_sample);
return run(slice, out);
}
FFTImpl::Result FFTImpl::run(span<const i16> sample, FFTBins *out) {
if (!mContext) {
return FFTImpl::Result(false, "FFTImpl context is not initialized");
}
if (sample.size() != mContext->sampleSize()) {
FASTLED_WARN("FFTImpl sample size mismatch");
return FFTImpl::Result(false, "FFTImpl sample size mismatch");
}
mContext->fft_unit_test(sample, out);
return FFTImpl::Result(true, "");
}
} // namespace fl

View File

@@ -0,0 +1,58 @@
#pragma once
#include "fl/hash_map_lru.h"
#include "fl/pair.h"
#include "fl/unique_ptr.h"
#include "fl/span.h"
#include "fl/vector.h"
namespace fl {
class AudioSample;
class FFTContext;
struct FFT_Args;
// Example:
// FFTImpl fft(512, 16);
// auto sample = SINE WAVE OF 512 SAMPLES
// fft.run(buffer, &out);
// FASTLED_WARN("FFTImpl output: " << out); // 16 bands of output.
class FFTImpl {
public:
// Result indicating success or failure of the FFTImpl run (in which case
// there will be an error message).
struct Result {
Result(bool ok, const string &error) : ok(ok), error(error) {}
bool ok = false;
fl::string error;
};
// Default values for the FFTImpl.
FFTImpl(const FFT_Args &args);
~FFTImpl();
fl::size sampleSize() const;
// Note that the sample sizes MUST match the samples size passed into the
// constructor.
Result run(const AudioSample &sample, FFTBins *out);
Result run(span<const i16> sample, FFTBins *out);
// Info on what the frequency the bins represent
fl::string info() const;
// Detail.
static int DefaultSamples() { return 512; }
static int DefaultBands() { return 16; }
static float DefaultMinFrequency() { return 174.6f; }
static float DefaultMaxFrequency() { return 4698.3f; }
static int DefaultSampleRate() { return 44100; }
// Disable copy and move constructors and assignment operators
FFTImpl(const FFTImpl &) = delete;
FFTImpl &operator=(const FFTImpl &) = delete;
FFTImpl(FFTImpl &&) = delete;
FFTImpl &operator=(FFTImpl &&) = delete;
private:
fl::unique_ptr<FFTContext> mContext;
};
}; // namespace fl

View File

@@ -0,0 +1,206 @@
#include "fl/file_system.h"
#include "fl/unused.h"
#include "fl/warn.h"
#include "fl/compiler_control.h"
#include "fl/has_include.h"
#ifdef __EMSCRIPTEN__
#include "platforms/wasm/fs_wasm.h"
#define FASTLED_HAS_SDCARD 1
#elif FL_HAS_INCLUDE(<SD.h>) && FL_HAS_INCLUDE(<fs.h>)
// Include Arduino SD card implementation when SD library is available
#include "platforms/fs_sdcard_arduino.hpp"
#define FASTLED_HAS_SDCARD 1
#else
#define FASTLED_HAS_SDCARD 0
#endif
#include "fl/json.h"
#include "fl/namespace.h"
#include "fl/screenmap.h"
#include "fl/unused.h"
namespace fl {
class NullFileHandle : public FileHandle {
public:
NullFileHandle() = default;
~NullFileHandle() override {}
bool available() const override { return false; }
fl::size size() const override { return 0; }
fl::size read(u8 *dst, fl::size bytesToRead) override {
FASTLED_UNUSED(dst);
FASTLED_UNUSED(bytesToRead);
return 0;
}
fl::size pos() const override { return 0; }
const char *path() const override { return "NULL FILE HANDLE"; }
bool seek(fl::size pos) override {
FASTLED_UNUSED(pos);
return false;
}
void close() override {}
bool valid() const override {
FASTLED_WARN("NullFileHandle is not valid");
return false;
}
};
class NullFileSystem : public FsImpl {
public:
NullFileSystem() {
FASTLED_WARN("NullFileSystem instantiated as a placeholder, please "
"implement a file system for your platform.");
}
~NullFileSystem() override {}
bool begin() override { return true; }
void end() override {}
void close(FileHandlePtr file) override {
// No need to do anything for in-memory files
FASTLED_UNUSED(file);
FASTLED_WARN("NullFileSystem::close");
}
FileHandlePtr openRead(const char *_path) override {
FASTLED_UNUSED(_path);
fl::shared_ptr<NullFileHandle> ptr = fl::make_shared<NullFileHandle>();
FileHandlePtr out = ptr;
return out;
}
};
bool FileSystem::beginSd(int cs_pin) {
mFs = make_sdcard_filesystem(cs_pin);
if (!mFs) {
return false;
}
mFs->begin();
return true;
}
bool FileSystem::begin(FsImplPtr platform_filesystem) {
mFs = platform_filesystem;
if (!mFs) {
return false;
}
mFs->begin();
return true;
}
fl::size FileHandle::bytesLeft() const { return size() - pos(); }
FileSystem::FileSystem() : mFs() {}
void FileSystem::end() {
if (mFs) {
mFs->end();
}
}
bool FileSystem::readJson(const char *path, Json *doc) {
string text;
if (!readText(path, &text)) {
return false;
}
// Parse using the new Json class
*doc = fl::Json::parse(text);
return !doc->is_null();
}
bool FileSystem::readScreenMaps(const char *path,
fl::fl_map<string, ScreenMap> *out, string *error) {
string text;
if (!readText(path, &text)) {
FASTLED_WARN("Failed to read file: " << path);
if (error) {
*error = "Failed to read file: ";
error->append(path);
}
return false;
}
string err;
bool ok = ScreenMap::ParseJson(text.c_str(), out, &err);
if (!ok) {
FASTLED_WARN("Failed to parse screen map: " << err.c_str());
*error = err;
return false;
}
return true;
}
bool FileSystem::readScreenMap(const char *path, const char *name,
ScreenMap *out, string *error) {
string text;
if (!readText(path, &text)) {
FASTLED_WARN("Failed to read file: " << path);
if (error) {
*error = "Failed to read file: ";
error->append(path);
}
return false;
}
string err;
bool ok = ScreenMap::ParseJson(text.c_str(), name, out, &err);
if (!ok) {
FASTLED_WARN("Failed to parse screen map: " << err.c_str());
*error = err;
return false;
}
return true;
}
void FileSystem::close(FileHandlePtr file) { mFs->close(file); }
FileHandlePtr FileSystem::openRead(const char *path) {
return mFs->openRead(path);
}
Video FileSystem::openVideo(const char *path, fl::size pixelsPerFrame, float fps,
fl::size nFrameHistory) {
Video video(pixelsPerFrame, fps, nFrameHistory);
FileHandlePtr file = openRead(path);
if (!file) {
video.setError(fl::string("Could not open file: ").append(path));
return video;
}
video.begin(file);
return video;
}
bool FileSystem::readText(const char *path, fl::string *out) {
FileHandlePtr file = openRead(path);
if (!file) {
FASTLED_WARN("Failed to open file: " << path);
return false;
}
fl::size size = file->size();
out->reserve(size + out->size());
bool wrote = false;
while (file->available()) {
u8 buf[64];
fl::size n = file->read(buf, sizeof(buf));
// out->append(buf, n);
out->append((const char *)buf, n);
wrote = true;
}
file->close();
FASTLED_DBG_IF(!wrote, "Failed to write any data to the output string.");
return wrote;
}
} // namespace fl
namespace fl {
#if !FASTLED_HAS_SDCARD
// Weak fallback implementation when SD library is not available
FL_LINK_WEAK FsImplPtr make_sdcard_filesystem(int cs_pin) {
FASTLED_UNUSED(cs_pin);
fl::shared_ptr<NullFileSystem> ptr = fl::make_shared<NullFileSystem>();
FsImplPtr out = ptr;
return out;
}
#endif
} // namespace fl

View File

@@ -0,0 +1,108 @@
#pragma once
// Note, fs.h breaks ESPAsyncWebServer so we use file_system.h instead.
#include "fl/stdint.h"
#include "fl/int.h"
#include "fl/namespace.h"
#include "fl/memory.h"
#include "fl/str.h"
#include "fx/video.h"
#include "fl/screenmap.h"
namespace fl {
FASTLED_SMART_PTR(FsImpl);
// PLATFORM INTERFACE
// You need to define this for your platform.
// Otherwise a null filesystem will be used that will do nothing but spew
// warnings, but otherwise won't crash the system.
FsImplPtr make_sdcard_filesystem(int cs_pin);
} // namespace fl
FASTLED_NAMESPACE_BEGIN
struct CRGB;
FASTLED_NAMESPACE_END
namespace fl {
class ScreenMap;
FASTLED_SMART_PTR(FileSystem);
FASTLED_SMART_PTR(FileHandle);
class Video;
template <typename Key, typename Value, fl::size N> class FixedMap;
namespace json2 {
class Json;
}
class FileSystem {
public:
FileSystem();
bool beginSd(int cs_pin); // Signal to begin using the filesystem resource.
bool begin(FsImplPtr platform_filesystem); // Signal to begin using the
// filesystem resource.
void end(); // Signal to end use of the file system.
FileHandlePtr
openRead(const char *path); // Null if file could not be opened.
Video
openVideo(const char *path, fl::size pixelsPerFrame, float fps = 30.0f,
fl::size nFrameHistory = 0); // Null if video could not be opened.
bool readText(const char *path, string *out);
bool readJson(const char *path, Json *doc);
bool readScreenMaps(const char *path, fl::fl_map<string, ScreenMap> *out,
string *error = nullptr);
bool readScreenMap(const char *path, const char *name, ScreenMap *out,
string *error = nullptr);
void close(FileHandlePtr file);
private:
FsImplPtr mFs; // System dependent filesystem.
};
// An abstract class that represents a file handle.
// Devices like the SD card will return one of these.
class FileHandle {
public:
virtual ~FileHandle() {}
virtual bool available() const = 0;
virtual fl::size bytesLeft() const;
virtual fl::size size() const = 0;
virtual fl::size read(fl::u8 *dst, fl::size bytesToRead) = 0;
virtual fl::size pos() const = 0;
virtual const char *path() const = 0;
virtual bool seek(fl::size pos) = 0;
virtual void close() = 0;
virtual bool valid() const = 0;
// convenience functions
fl::size readCRGB(CRGB *dst, fl::size n) {
return read((fl::u8 *)dst, n * 3) / 3;
}
};
// Platforms will subclass this to implement the filesystem.
class FsImpl {
public:
struct Visitor {
virtual ~Visitor() {}
virtual void accept(const char *path) = 0;
};
FsImpl() = default;
virtual ~FsImpl() {} // Use default pins for spi.
virtual bool begin() = 0;
// End use of card
virtual void end() = 0;
virtual void close(FileHandlePtr file) = 0;
virtual FileHandlePtr openRead(const char *path) = 0;
virtual bool ls(Visitor &visitor) {
// todo: implement.
(void)visitor;
return false;
}
};
} // namespace fl

View File

@@ -0,0 +1,173 @@
#include "fl/stdint.h"
#include "fill.h"
namespace fl {
void fill_solid(struct CRGB *targetArray, int numToFill,
const struct CRGB &color) {
for (int i = 0; i < numToFill; ++i) {
targetArray[i] = color;
}
}
void fill_solid(struct CHSV *targetArray, int numToFill,
const struct CHSV &color) {
for (int i = 0; i < numToFill; ++i) {
targetArray[i] = color;
}
}
// void fill_solid( struct CRGB* targetArray, int numToFill,
// const struct CHSV& hsvColor)
// {
// fill_solid<CRGB>( targetArray, numToFill, (CRGB) hsvColor);
// }
void fill_rainbow(struct CRGB *targetArray, int numToFill, u8 initialhue,
u8 deltahue) {
CHSV hsv;
hsv.hue = initialhue;
hsv.val = 255;
hsv.sat = 240;
for (int i = 0; i < numToFill; ++i) {
targetArray[i] = hsv;
hsv.hue += deltahue;
}
}
void fill_rainbow(struct CHSV *targetArray, int numToFill, u8 initialhue,
u8 deltahue) {
CHSV hsv;
hsv.hue = initialhue;
hsv.val = 255;
hsv.sat = 240;
for (int i = 0; i < numToFill; ++i) {
targetArray[i] = hsv;
hsv.hue += deltahue;
}
}
void fill_rainbow_circular(struct CRGB *targetArray, int numToFill,
u8 initialhue, bool reversed) {
if (numToFill == 0)
return; // avoiding div/0
CHSV hsv;
hsv.hue = initialhue;
hsv.val = 255;
hsv.sat = 240;
const u16 hueChange =
65535 / (u16)numToFill; // hue change for each LED, * 256 for
// precision (256 * 256 - 1)
u16 hueOffset = 0; // offset for hue value, with precision (*256)
for (int i = 0; i < numToFill; ++i) {
targetArray[i] = hsv;
if (reversed)
hueOffset -= hueChange;
else
hueOffset += hueChange;
hsv.hue = initialhue +
(u8)(hueOffset >>
8); // assign new hue with precise offset (as 8-bit)
}
}
void fill_rainbow_circular(struct CHSV *targetArray, int numToFill,
u8 initialhue, bool reversed) {
if (numToFill == 0)
return; // avoiding div/0
CHSV hsv;
hsv.hue = initialhue;
hsv.val = 255;
hsv.sat = 240;
const u16 hueChange =
65535 / (u16)numToFill; // hue change for each LED, * 256 for
// precision (256 * 256 - 1)
u16 hueOffset = 0; // offset for hue value, with precision (*256)
for (int i = 0; i < numToFill; ++i) {
targetArray[i] = hsv;
if (reversed)
hueOffset -= hueChange;
else
hueOffset += hueChange;
hsv.hue = initialhue +
(u8)(hueOffset >>
8); // assign new hue with precise offset (as 8-bit)
}
}
void fill_gradient_RGB(CRGB *leds, u16 startpos, CRGB startcolor,
u16 endpos, CRGB endcolor) {
// if the points are in the wrong order, straighten them
if (endpos < startpos) {
u16 t = endpos;
CRGB tc = endcolor;
endcolor = startcolor;
endpos = startpos;
startpos = t;
startcolor = tc;
}
saccum87 rdistance87;
saccum87 gdistance87;
saccum87 bdistance87;
rdistance87 = (endcolor.r - startcolor.r) << 7;
gdistance87 = (endcolor.g - startcolor.g) << 7;
bdistance87 = (endcolor.b - startcolor.b) << 7;
u16 pixeldistance = endpos - startpos;
i16 divisor = pixeldistance ? pixeldistance : 1;
saccum87 rdelta87 = rdistance87 / divisor;
saccum87 gdelta87 = gdistance87 / divisor;
saccum87 bdelta87 = bdistance87 / divisor;
rdelta87 *= 2;
gdelta87 *= 2;
bdelta87 *= 2;
accum88 r88 = startcolor.r << 8;
accum88 g88 = startcolor.g << 8;
accum88 b88 = startcolor.b << 8;
for (u16 i = startpos; i <= endpos; ++i) {
leds[i] = CRGB(r88 >> 8, g88 >> 8, b88 >> 8);
r88 += rdelta87;
g88 += gdelta87;
b88 += bdelta87;
}
}
void fill_gradient_RGB(CRGB *leds, u16 numLeds, const CRGB &c1,
const CRGB &c2) {
u16 last = numLeds - 1;
fill_gradient_RGB(leds, 0, c1, last, c2);
}
void fill_gradient_RGB(CRGB *leds, u16 numLeds, const CRGB &c1,
const CRGB &c2, const CRGB &c3) {
u16 half = (numLeds / 2);
u16 last = numLeds - 1;
fill_gradient_RGB(leds, 0, c1, half, c2);
fill_gradient_RGB(leds, half, c2, last, c3);
}
void fill_gradient_RGB(CRGB *leds, u16 numLeds, const CRGB &c1,
const CRGB &c2, const CRGB &c3, const CRGB &c4) {
u16 onethird = (numLeds / 3);
u16 twothirds = ((numLeds * 2) / 3);
u16 last = numLeds - 1;
fill_gradient_RGB(leds, 0, c1, onethird, c2);
fill_gradient_RGB(leds, onethird, c2, twothirds, c3);
fill_gradient_RGB(leds, twothirds, c3, last, c4);
}
} // namespace fl

View File

@@ -0,0 +1,291 @@
#pragma once
#include "crgb.h"
#include "fl/colorutils_misc.h"
#include "fl/int.h"
#include "fl/stdint.h"
#include "fl/compiler_control.h"
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_UNUSED_PARAMETER
FL_DISABLE_WARNING_RETURN_TYPE
FL_DISABLE_WARNING_IMPLICIT_INT_CONVERSION
FL_DISABLE_WARNING_FLOAT_CONVERSION
FL_DISABLE_WARNING_SIGN_CONVERSION
/// ANSI: signed short _Accum. 8 bits int, 7 bits fraction
/// @see accum88
#define saccum87 i16
namespace fl {
/// @defgroup ColorUtils Color Utility Functions
/// A variety of functions for working with color, palettes, and leds
/// @{
/// @defgroup ColorFills Color Fill Functions
/// Functions for filling LED arrays with colors and gradients
/// @{
/// Fill a range of LEDs with a solid color.
/// @param targetArray a pointer to the LED array to fill
/// @param numToFill the number of LEDs to fill in the array
/// @param color the color to fill with
void fill_solid(struct CRGB *targetArray, int numToFill,
const struct CRGB &color);
/// @copydoc fill_solid()
void fill_solid(struct CHSV *targetArray, int numToFill,
const struct CHSV &color);
/// Fill a range of LEDs with a rainbow of colors.
/// The colors making up the rainbow are at full saturation and full
/// value (brightness).
/// @param targetArray a pointer to the LED array to fill
/// @param numToFill the number of LEDs to fill in the array
/// @param initialhue the starting hue for the rainbow
/// @param deltahue how many hue values to advance for each LED
void fill_rainbow(struct CRGB *targetArray, int numToFill, fl::u8 initialhue,
fl::u8 deltahue = 5);
/// @copydoc fill_rainbow()
void fill_rainbow(struct CHSV *targetArray, int numToFill, fl::u8 initialhue,
fl::u8 deltahue = 5);
/// Fill a range of LEDs with a rainbow of colors, so that the hues
/// are continuous between the end of the strip and the beginning.
/// The colors making up the rainbow are at full saturation and full
/// value (brightness).
/// @param targetArray a pointer to the LED array to fill
/// @param numToFill the number of LEDs to fill in the array
/// @param initialhue the starting hue for the rainbow
/// @param reversed whether to progress through the rainbow hues backwards
void fill_rainbow_circular(struct CRGB *targetArray, int numToFill,
fl::u8 initialhue, bool reversed = false);
/// @copydoc fill_rainbow_circular()
void fill_rainbow_circular(struct CHSV *targetArray, int numToFill,
fl::u8 initialhue, bool reversed = false);
/// Fill a range of LEDs with a smooth HSV gradient between two HSV colors.
/// This function can write the gradient colors either:
///
/// 1. Into an array of CRGBs (e.g., an leds[] array, or a CRGB palette)
/// 2. Into an array of CHSVs (e.g. a CHSV palette).
///
/// In the case of writing into a CRGB array, the gradient is
/// computed in HSV space, and then HSV values are converted to RGB
/// as they're written into the CRGB array.
/// @param targetArray a pointer to the color array to fill
/// @param startpos the starting position in the array
/// @param startcolor the starting color for the gradient
/// @param endpos the ending position in the array
/// @param endcolor the end color for the gradient
/// @param directionCode the direction to travel around the color wheel
template <typename T>
void fill_gradient(T *targetArray, u16 startpos, CHSV startcolor,
u16 endpos, CHSV endcolor,
TGradientDirectionCode directionCode = SHORTEST_HUES) {
// if the points are in the wrong order, straighten them
if (endpos < startpos) {
u16 t = endpos;
CHSV tc = endcolor;
endcolor = startcolor;
endpos = startpos;
startpos = t;
startcolor = tc;
}
// If we're fading toward black (val=0) or white (sat=0),
// then set the endhue to the starthue.
// This lets us ramp smoothly to black or white, regardless
// of what 'hue' was set in the endcolor (since it doesn't matter)
if (endcolor.value == 0 || endcolor.saturation == 0) {
endcolor.hue = startcolor.hue;
}
// Similarly, if we're fading in from black (val=0) or white (sat=0)
// then set the starthue to the endhue.
// This lets us ramp smoothly up from black or white, regardless
// of what 'hue' was set in the startcolor (since it doesn't matter)
if (startcolor.value == 0 || startcolor.saturation == 0) {
startcolor.hue = endcolor.hue;
}
saccum87 huedistance87;
saccum87 satdistance87;
saccum87 valdistance87;
satdistance87 = (endcolor.sat - startcolor.sat) << 7;
valdistance87 = (endcolor.val - startcolor.val) << 7;
fl::u8 huedelta8 = endcolor.hue - startcolor.hue;
if (directionCode == SHORTEST_HUES) {
directionCode = FORWARD_HUES;
if (huedelta8 > 127) {
directionCode = BACKWARD_HUES;
}
}
if (directionCode == LONGEST_HUES) {
directionCode = FORWARD_HUES;
if (huedelta8 < 128) {
directionCode = BACKWARD_HUES;
}
}
if (directionCode == FORWARD_HUES) {
huedistance87 = huedelta8 << 7;
} else /* directionCode == BACKWARD_HUES */
{
huedistance87 = (fl::u8)(256 - huedelta8) << 7;
huedistance87 = -huedistance87;
}
u16 pixeldistance = endpos - startpos;
i16 divisor = pixeldistance ? pixeldistance : 1;
#if FASTLED_USE_32_BIT_GRADIENT_FILL
// Use higher precision 32 bit math for new micros.
i32 huedelta823 = (huedistance87 * 65536) / divisor;
i32 satdelta823 = (satdistance87 * 65536) / divisor;
i32 valdelta823 = (valdistance87 * 65536) / divisor;
huedelta823 *= 2;
satdelta823 *= 2;
valdelta823 *= 2;
u32 hue824 = static_cast<u32>(startcolor.hue) << 24;
u32 sat824 = static_cast<u32>(startcolor.sat) << 24;
u32 val824 = static_cast<u32>(startcolor.val) << 24;
for (u16 i = startpos; i <= endpos; ++i) {
targetArray[i] = CHSV(hue824 >> 24, sat824 >> 24, val824 >> 24);
hue824 += huedelta823;
sat824 += satdelta823;
val824 += valdelta823;
}
#else
// Use 8-bit math for older micros.
saccum87 huedelta87 = huedistance87 / divisor;
saccum87 satdelta87 = satdistance87 / divisor;
saccum87 valdelta87 = valdistance87 / divisor;
huedelta87 *= 2;
satdelta87 *= 2;
valdelta87 *= 2;
accum88 hue88 = startcolor.hue << 8;
accum88 sat88 = startcolor.sat << 8;
accum88 val88 = startcolor.val << 8;
for (u16 i = startpos; i <= endpos; ++i) {
targetArray[i] = CHSV(hue88 >> 8, sat88 >> 8, val88 >> 8);
hue88 += huedelta87;
sat88 += satdelta87;
val88 += valdelta87;
}
#endif // defined(__AVR__)
}
/// Fill a range of LEDs with a smooth HSV gradient between two HSV colors.
/// @see fill_gradient()
/// @param targetArray a pointer to the color array to fill
/// @param numLeds the number of LEDs to fill
/// @param c1 the starting color in the gradient
/// @param c2 the end color for the gradient
/// @param directionCode the direction to travel around the color wheel
template <typename T>
void fill_gradient(T *targetArray, u16 numLeds, const CHSV &c1,
const CHSV &c2,
TGradientDirectionCode directionCode = SHORTEST_HUES) {
u16 last = numLeds - 1;
fill_gradient(targetArray, 0, c1, last, c2, directionCode);
}
/// Fill a range of LEDs with a smooth HSV gradient between three HSV colors.
/// @see fill_gradient()
/// @param targetArray a pointer to the color array to fill
/// @param numLeds the number of LEDs to fill
/// @param c1 the starting color in the gradient
/// @param c2 the middle color for the gradient
/// @param c3 the end color for the gradient
/// @param directionCode the direction to travel around the color wheel
template <typename T>
void fill_gradient(T *targetArray, u16 numLeds, const CHSV &c1,
const CHSV &c2, const CHSV &c3,
TGradientDirectionCode directionCode = SHORTEST_HUES) {
u16 half = (numLeds / 2);
u16 last = numLeds - 1;
fill_gradient(targetArray, 0, c1, half, c2, directionCode);
fill_gradient(targetArray, half, c2, last, c3, directionCode);
}
/// Fill a range of LEDs with a smooth HSV gradient between four HSV colors.
/// @see fill_gradient()
/// @param targetArray a pointer to the color array to fill
/// @param numLeds the number of LEDs to fill
/// @param c1 the starting color in the gradient
/// @param c2 the first middle color for the gradient
/// @param c3 the second middle color for the gradient
/// @param c4 the end color for the gradient
/// @param directionCode the direction to travel around the color wheel
template <typename T>
void fill_gradient(T *targetArray, u16 numLeds, const CHSV &c1,
const CHSV &c2, const CHSV &c3, const CHSV &c4,
TGradientDirectionCode directionCode = SHORTEST_HUES) {
u16 onethird = (numLeds / 3);
u16 twothirds = ((numLeds * 2) / 3);
u16 last = numLeds - 1;
fill_gradient(targetArray, 0, c1, onethird, c2, directionCode);
fill_gradient(targetArray, onethird, c2, twothirds, c3, directionCode);
fill_gradient(targetArray, twothirds, c3, last, c4, directionCode);
}
/// Convenience synonym
#define fill_gradient_HSV fill_gradient
/// Fill a range of LEDs with a smooth RGB gradient between two RGB colors.
/// Unlike HSV, there is no "color wheel" in RGB space, and therefore there's
/// only one "direction" for the gradient to go. This means there's no
/// TGradientDirectionCode parameter for direction.
/// @param leds a pointer to the LED array to fill
/// @param startpos the starting position in the array
/// @param startcolor the starting color for the gradient
/// @param endpos the ending position in the array
/// @param endcolor the end color for the gradient
void fill_gradient_RGB(CRGB *leds, u16 startpos, CRGB startcolor,
u16 endpos, CRGB endcolor);
/// Fill a range of LEDs with a smooth RGB gradient between two RGB colors.
/// @see fill_gradient_RGB()
/// @param leds a pointer to the LED array to fill
/// @param numLeds the number of LEDs to fill
/// @param c1 the starting color in the gradient
/// @param c2 the end color for the gradient
void fill_gradient_RGB(CRGB *leds, u16 numLeds, const CRGB &c1,
const CRGB &c2);
/// Fill a range of LEDs with a smooth RGB gradient between three RGB colors.
/// @see fill_gradient_RGB()
/// @param leds a pointer to the LED array to fill
/// @param numLeds the number of LEDs to fill
/// @param c1 the starting color in the gradient
/// @param c2 the middle color for the gradient
/// @param c3 the end color for the gradient
void fill_gradient_RGB(CRGB *leds, u16 numLeds, const CRGB &c1,
const CRGB &c2, const CRGB &c3);
/// Fill a range of LEDs with a smooth RGB gradient between four RGB colors.
/// @see fill_gradient_RGB()
/// @param leds a pointer to the LED array to fill
/// @param numLeds the number of LEDs to fill
/// @param c1 the starting color in the gradient
/// @param c2 the first middle color for the gradient
/// @param c3 the second middle color for the gradient
/// @param c4 the end color for the gradient
void fill_gradient_RGB(CRGB *leds, u16 numLeds, const CRGB &c1,
const CRGB &c2, const CRGB &c3, const CRGB &c4);
} // namespace fl
FL_DISABLE_WARNING_POP

View File

@@ -0,0 +1,191 @@
/// @file five_bit_hd_gamma.h
/// Declares functions for five-bit gamma correction
#pragma once
#include "fl/gamma.h"
#include "fl/int.h"
#include "fl/math.h"
#include "crgb.h"
#include "lib8tion/scale8.h"
namespace fl {
enum FiveBitGammaCorrectionMode {
kFiveBitGammaCorrectionMode_Null = 0,
kFiveBitGammaCorrectionMode_BitShift = 1
};
// Applies gamma correction for the RGBV(8, 8, 8, 5) color space, where
// the last byte is the brightness byte at 5 bits.
// To override this five_bit_hd_gamma_bitshift you'll need to define
// FASTLED_FIVE_BIT_HD_BITSHIFT_FUNCTION_OVERRIDE in your build settings
// then define the function anywhere in your project.
// Example:
// FASTLED_NAMESPACE_BEGIN
// void five_bit_hd_gamma_bitshift(
// fl::u8 r8, fl::u8 g8, fl::u8 b8,
// fl::u8 r8_scale, fl::u8 g8_scale, fl::u8 b8_scale,
// fl::u8* out_r8,
// fl::u8* out_g8,
// fl::u8* out_b8,
// fl::u8* out_power_5bit) {
// cout << "hello world\n";
// }
// FASTLED_NAMESPACE_END
// Force push
void internal_builtin_five_bit_hd_gamma_bitshift(CRGB colors, CRGB colors_scale,
fl::u8 global_brightness,
CRGB *out_colors,
fl::u8 *out_power_5bit);
// Exposed for testing.
void five_bit_bitshift(u16 r16, u16 g16, u16 b16, fl::u8 brightness, CRGB *out,
fl::u8 *out_power_5bit);
#ifdef FASTLED_FIVE_BIT_HD_BITSHIFT_FUNCTION_OVERRIDE
// This function is located somewhere else in your project, so it's declared
// extern here.
extern void five_bit_hd_gamma_bitshift(CRGB colors, CRGB colors_scale,
fl::u8 global_brightness,
CRGB *out_colors,
fl::u8 *out_power_5bit);
#else
inline void five_bit_hd_gamma_bitshift(CRGB colors, CRGB colors_scale,
fl::u8 global_brightness,
CRGB *out_colors,
fl::u8 *out_power_5bit) {
internal_builtin_five_bit_hd_gamma_bitshift(
colors, colors_scale, global_brightness, out_colors, out_power_5bit);
}
#endif // FASTLED_FIVE_BIT_HD_BITSHIFT_FUNCTION_OVERRIDE
// Simple gamma correction function that converts from
// 8-bit color component and converts it to gamma corrected 16-bit
// color component. Fast and no memory overhead!
// To override this function you'll need to define
// FASTLED_FIVE_BIT_HD_GAMMA_BITSHIFT_FUNCTION_OVERRIDE in your build settings
// and then define your own version anywhere in your project. Example:
// FASTLED_NAMESPACE_BEGIN
// void five_bit_hd_gamma_function(
// fl::u8 r8, fl::u8 g8, fl::u8 b8,
// u16* r16, u16* g16, u16* b16) {
// cout << "hello world\n";
// }
// FASTLED_NAMESPACE_END
#ifdef FASTLED_FIVE_BIT_HD_GAMMA_FUNCTION_OVERRIDE
// This function is located somewhere else in your project, so it's declared
// extern here.
extern void five_bit_hd_gamma_function(CRGB color, u16 *r16, u16 *g16,
u16 *b16);
#else
inline void five_bit_hd_gamma_function(CRGB color, u16 *r16, u16 *g16,
u16 *b16) {
gamma16(color, r16, g16, b16);
}
#endif // FASTLED_FIVE_BIT_HD_GAMMA_FUNCTION_OVERRIDE
inline void internal_builtin_five_bit_hd_gamma_bitshift(
CRGB colors, CRGB colors_scale, fl::u8 global_brightness, CRGB *out_colors,
fl::u8 *out_power_5bit) {
if (global_brightness == 0) {
*out_colors = CRGB(0, 0, 0);
*out_power_5bit = 0;
return;
}
// Step 1: Gamma Correction
u16 r16, g16, b16;
five_bit_hd_gamma_function(colors, &r16, &g16, &b16);
// Step 2: Color correction step comes after gamma correction. These values
// are assumed to be be relatively close to 255.
if (colors_scale.r != 0xff) {
r16 = scale16by8(r16, colors_scale.r);
}
if (colors_scale.g != 0xff) {
g16 = scale16by8(g16, colors_scale.g);
}
if (colors_scale.b != 0xff) {
b16 = scale16by8(b16, colors_scale.b);
}
five_bit_bitshift(r16, g16, b16, global_brightness, out_colors,
out_power_5bit);
}
// Since the return value wasn't used, it has been omitted.
// It's not clear what scale brightness is, or how it is to be applied,
// so we assume 8 bits applied over the given rgb values.
inline void five_bit_bitshift(uint16_t r16, uint16_t g16, uint16_t b16,
uint8_t brightness, CRGB *out,
uint8_t *out_power_5bit) {
// NEW in 3.10.2: A new closed form solution has been found!
// Thank you https://github.com/gwgill!
// It's okay if you don't know how this works, few do, but it tests
// very well and is better than the old iterative approach which had
// bad quantization issues (sudden jumps in brightness in certain intervals).
// ix/31 * 255/65536 * 256 scaling factors, valid for indexes 1..31
static uint32_t bright_scale[32] = {
0, 2023680, 1011840, 674560, 505920, 404736, 337280, 289097,
252960, 224853, 202368, 183971, 168640, 155668, 144549, 134912,
126480, 119040, 112427, 106509, 101184, 96366, 91985, 87986,
84320, 80947, 77834, 74951, 72274, 69782, 67456, 65280};
auto max3 = [](u16 a, u16 b, u16 c) { return fl_max(fl_max(a, b), c); };
if (brightness == 0) {
*out = CRGB(0, 0, 0);
*out_power_5bit = 0;
return;
}
if (r16 == 0 && g16 == 0 && b16 == 0) {
*out = CRGB(0, 0, 0);
*out_power_5bit = (brightness <= 31) ? brightness : 31;
return;
}
uint8_t r8 = 0, g8 = 0, b8 = 0;
// Apply any brightness setting (we assume brightness is 0..255)
if (brightness != 0xff) {
r16 = scale16by8(r16, brightness);
g16 = scale16by8(g16, brightness);
b16 = scale16by8(b16, brightness);
}
// Locate the largest value to set the brightness/scale factor
uint16_t scale = max3(r16, g16, b16);
if (scale == 0) {
*out = CRGB(0, 0, 0);
*out_power_5bit = 0;
return;
} else {
uint32_t scalef;
// Compute 5 bit quantized scale that is at or above the maximum value.
scale = (scale + (2047 - (scale >> 5))) >> 11;
// Adjust the 16 bit values to account for the scale, then round to 8
// bits
scalef = bright_scale[scale];
r8 = (r16 * scalef + 0x808000) >> 24;
g8 = (g16 * scalef + 0x808000) >> 24;
b8 = (b16 * scalef + 0x808000) >> 24;
*out = CRGB(r8, g8, b8);
*out_power_5bit = static_cast<uint8_t>(scale);
return;
}
}
} // namespace fl

View File

@@ -0,0 +1,7 @@
#pragma once
#ifdef FASTLED_NO_FORCE_INLINE
#define FASTLED_FORCE_INLINE inline
#else
#define FASTLED_FORCE_INLINE __attribute__((always_inline)) inline
#endif

View File

@@ -0,0 +1,351 @@
#pragma once
#include "fl/memory.h"
#include "fl/type_traits.h"
#include "fl/compiler_control.h"
#include "fl/variant.h"
#include "fl/memfill.h"
#include "fl/type_traits.h"
#include "fl/inplacenew.h"
#include "fl/bit_cast.h"
#include "fl/align.h"
#ifndef FASTLED_INLINE_LAMBDA_SIZE
#define FASTLED_INLINE_LAMBDA_SIZE 64
#endif
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING(float-equal)
namespace fl {
//----------------------------------------------------------------------------
// is_function_pointer trait - detects function pointers like R(*)(Args...)
//----------------------------------------------------------------------------
template <typename T> struct is_function_pointer {
static constexpr bool value = false;
};
template <typename R, typename... Args>
struct is_function_pointer<R(*)(Args...)> {
static constexpr bool value = true;
};
//----------------------------------------------------------------------------
// More or less a drop in replacement for std::function
// function<R(Args...)>: typeerasing "std::function" replacement
// Supports free functions, lambdas/functors, member functions (const &
// nonconst)
//
// NEW: Uses inline storage for member functions, free functions, and small
// lambdas/functors. Only large lambdas/functors use heap allocation.
//----------------------------------------------------------------------------
template <typename> class function;
template <typename R, typename... Args>
class FL_ALIGN function<R(Args...)> {
private:
struct CallableBase {
virtual R invoke(Args... args) = 0;
virtual ~CallableBase() = default;
};
template <typename F>
struct Callable : CallableBase {
F f;
Callable(F fn) : f(fn) {}
R invoke(Args... args) override { return f(args...); }
};
// Type-erased free function callable - stored inline!
struct FreeFunctionCallable {
R (*func_ptr)(Args...);
FreeFunctionCallable(R (*fp)(Args...)) : func_ptr(fp) {}
R invoke(Args... args) const {
return func_ptr(args...);
}
};
// Type-erased small lambda/functor callable - stored inline!
// Size limit for inline storage - configurable via preprocessor define
static constexpr fl::size kInlineLambdaSize = FASTLED_INLINE_LAMBDA_SIZE;
struct InlinedLambda {
// Storage for the lambda/functor object
// Use aligned storage to ensure proper alignment for any type
alignas(max_align_t) char bytes[kInlineLambdaSize];
// Type-erased invoker and destructor function pointers
R (*invoker)(const InlinedLambda& storage, Args... args);
void (*destructor)(InlinedLambda& storage);
template <typename Function>
InlinedLambda(Function f) {
static_assert(sizeof(Function) <= kInlineLambdaSize,
"Lambda/functor too large for inline storage");
static_assert(alignof(Function) <= alignof(max_align_t),
"Lambda/functor requires stricter alignment than storage provides");
// Initialize the entire storage to zero to avoid copying uninitialized memory
fl::memfill(bytes, 0, kInlineLambdaSize);
// Construct the lambda/functor in-place
new (bytes) Function(fl::move(f));
// Set up type-erased function pointers
invoker = &invoke_lambda<Function>;
destructor = &destroy_lambda<Function>;
}
// Copy constructor
InlinedLambda(const InlinedLambda& other)
: invoker(other.invoker), destructor(other.destructor) {
// This is tricky - we need to copy the stored object
// For now, we'll use memcopy (works for trivially copyable types)
fl::memcopy(bytes, other.bytes, kInlineLambdaSize);
}
// Move constructor
InlinedLambda(InlinedLambda&& other)
: invoker(other.invoker), destructor(other.destructor) {
fl::memcopy(bytes, other.bytes, kInlineLambdaSize);
// Reset the other object to prevent double destruction
other.destructor = nullptr;
}
~InlinedLambda() {
if (destructor) {
destructor(*this);
}
}
template <typename FUNCTOR>
static R invoke_lambda(const InlinedLambda& storage, Args... args) {
// Use placement new to safely access the stored lambda
alignas(FUNCTOR) char temp_storage[sizeof(FUNCTOR)];
// Copy the lambda from storage
fl::memcopy(temp_storage, storage.bytes, sizeof(FUNCTOR));
// Get a properly typed pointer to the copied lambda (non-const for mutable lambdas)
FUNCTOR* f = static_cast<FUNCTOR*>(static_cast<void*>(temp_storage));
// Invoke the lambda
return (*f)(args...);
}
template <typename FUNCTOR>
static void destroy_lambda(InlinedLambda& storage) {
// For destruction, we need to call the destructor on the actual object
// that was constructed with placement new in storage.bytes
// We use the standard library approach: create a properly typed pointer
// using placement new, then call the destructor through that pointer
// This is the standard-compliant way to get a properly typed pointer
// to an object that was constructed with placement new
FUNCTOR* obj_ptr = static_cast<FUNCTOR*>(static_cast<void*>(storage.bytes));
obj_ptr->~FUNCTOR();
}
R invoke(Args... args) const {
return invoker(*this, args...);
}
};
// Type-erased member function callable base
struct MemberCallableBase {
virtual R invoke(Args... args) const = 0;
virtual ~MemberCallableBase() = default;
};
// Type-erased non-const member function callable
struct NonConstMemberCallable : MemberCallableBase {
void* obj;
// Union to store member function pointer as raw bytes
union MemberFuncStorage {
char bytes[sizeof(R (NonConstMemberCallable::*)(Args...))];
// Ensure proper alignment
void* alignment_dummy;
} member_func_storage;
// Type-erased invoker function - set at construction time
R (*invoker)(void* obj, const MemberFuncStorage& mfp, Args... args);
template <typename C>
NonConstMemberCallable(C* o, R (C::*mf)(Args...)) : obj(o) {
// Store the member function pointer as raw bytes
static_assert(sizeof(mf) <= sizeof(member_func_storage),
"Member function pointer too large");
fl::memcopy(member_func_storage.bytes, &mf, sizeof(mf));
// Set the invoker to a function that knows how to cast back and call
invoker = &invoke_nonconst_member<C>;
}
template <typename C>
static R invoke_nonconst_member(void* obj, const MemberFuncStorage& mfp, Args... args) {
C* typed_obj = static_cast<C*>(obj);
R (C::*typed_mf)(Args...);
fl::memcopy(&typed_mf, mfp.bytes, sizeof(typed_mf));
return (typed_obj->*typed_mf)(args...);
}
R invoke(Args... args) const override {
return invoker(obj, member_func_storage, args...);
}
};
// Type-erased const member function callable
struct ConstMemberCallable : MemberCallableBase {
const void* obj;
// Union to store member function pointer as raw bytes
union MemberFuncStorage {
char bytes[sizeof(R (ConstMemberCallable::*)(Args...) const)];
// Ensure proper alignment
void* alignment_dummy;
} member_func_storage;
// Type-erased invoker function - set at construction time
R (*invoker)(const void* obj, const MemberFuncStorage& mfp, Args... args);
template <typename C>
ConstMemberCallable(const C* o, R (C::*mf)(Args...) const) : obj(o) {
// Store the member function pointer as raw bytes
static_assert(sizeof(mf) <= sizeof(member_func_storage),
"Member function pointer too large");
fl::memcopy(member_func_storage.bytes, &mf, sizeof(mf));
// Set the invoker to a function that knows how to cast back and call
invoker = &invoke_const_member<C>;
}
template <typename C>
static R invoke_const_member(const void* obj, const MemberFuncStorage& mfp, Args... args) {
const C* typed_obj = static_cast<const C*>(obj);
R (C::*typed_mf)(Args...) const;
fl::memcopy(&typed_mf, mfp.bytes, sizeof(typed_mf));
return (typed_obj->*typed_mf)(args...);
}
R invoke(Args... args) const override {
return invoker(obj, member_func_storage, args...);
}
};
// Variant to store any of our callable types inline (with heap fallback for large lambdas)
using Storage = Variant<fl::shared_ptr<CallableBase>, FreeFunctionCallable, InlinedLambda, NonConstMemberCallable, ConstMemberCallable>;
Storage storage_;
// Helper function to handle default return value for void and non-void types
template<typename ReturnType>
typename enable_if<!is_void<ReturnType>::value, ReturnType>::type
default_return_helper() const {
return ReturnType{};
}
template<typename ReturnType>
typename enable_if<is_void<ReturnType>::value, ReturnType>::type
default_return_helper() const {
return;
}
public:
function() = default;
// Copy constructor - properly handle Variant alignment
function(const function& other) : storage_(other.storage_) {}
// Move constructor - properly handle Variant alignment
function(function&& other) noexcept : storage_(fl::move(other.storage_)) {}
// Copy assignment
function& operator=(const function& other) {
if (this != &other) {
storage_ = other.storage_;
}
return *this;
}
// Move assignment
function& operator=(function&& other) noexcept {
if (this != &other) {
storage_ = fl::move(other.storage_);
}
return *this;
}
// 1) Free function constructor - stored inline!
function(R (*fp)(Args...)) {
storage_ = FreeFunctionCallable(fp);
}
// 2) Lambda/functor constructor - inline if small, heap if large
template <typename F, typename = enable_if_t<!is_member_function_pointer<F>::value && !is_function_pointer<F>::value>>
function(F f) {
// Use template specialization instead of if constexpr for C++14 compatibility
construct_lambda_or_functor(fl::move(f), typename conditional<sizeof(F) <= kInlineLambdaSize, true_type, false_type>::type{});
}
// 3) nonconst member function - stored inline!
template <typename C>
function(R (C::*mf)(Args...), C* obj) {
storage_ = NonConstMemberCallable(obj, mf);
}
// 4) const member function - stored inline!
template <typename C>
function(R (C::*mf)(Args...) const, const C* obj) {
storage_ = ConstMemberCallable(obj, mf);
}
R operator()(Args... args) const {
// Direct dispatch using type checking - efficient and simple
if (auto* heap_callable = storage_.template ptr<fl::shared_ptr<CallableBase>>()) {
return (*heap_callable)->invoke(args...);
} else if (auto* free_func = storage_.template ptr<FreeFunctionCallable>()) {
return free_func->invoke(args...);
} else if (auto* inlined_lambda = storage_.template ptr<InlinedLambda>()) {
return inlined_lambda->invoke(args...);
} else if (auto* nonconst_member = storage_.template ptr<NonConstMemberCallable>()) {
return nonconst_member->invoke(args...);
} else if (auto* const_member = storage_.template ptr<ConstMemberCallable>()) {
return const_member->invoke(args...);
}
// This should never happen if the function is properly constructed
return default_return_helper<R>();
}
explicit operator bool() const {
return !storage_.empty();
}
void clear() {
storage_ = Storage{}; // Reset to empty variant
}
bool operator==(const function& o) const {
// For simplicity, just check if both are empty or both are non-empty
// Full equality would require more complex comparison logic
return storage_.empty() == o.storage_.empty();
}
bool operator!=(const function& o) const {
return !(*this == o);
}
private:
// Helper for small lambdas/functors - inline storage
template <typename F>
void construct_lambda_or_functor(F f, true_type /* small */) {
storage_ = InlinedLambda(fl::move(f));
}
// Helper for large lambdas/functors - heap storage
template <typename F>
void construct_lambda_or_functor(F f, false_type /* large */) {
storage_ = fl::shared_ptr<CallableBase>(fl::make_shared<Callable<F>>(fl::move(f)));
}
};
} // namespace fl
FL_DISABLE_WARNING_POP

View File

@@ -0,0 +1,87 @@
#pragma once
#include "fl/function.h"
#include "fl/pair.h"
#include "fl/vector.h"
#include "fl/type_traits.h"
namespace fl {
template <typename FunctionType> class FunctionListBase {
protected:
fl::vector<pair<int, FunctionType>> mFunctions;
int mCounter = 0;
public:
// Iterator types
using iterator = typename fl::vector<pair<int, FunctionType>>::iterator;
using const_iterator = typename fl::vector<pair<int, FunctionType>>::const_iterator;
FunctionListBase() = default;
~FunctionListBase() = default;
int add(FunctionType function) {
int id = mCounter++;
pair<int, FunctionType> entry(id, function);
mFunctions.push_back(entry);
return id;
}
void remove(int id) {
for (int i = mFunctions.size() - 1; i >= 0; --i) {
if (mFunctions[i].first == id) {
mFunctions.erase(mFunctions.begin() + i);
}
}
}
void clear() { mFunctions.clear(); }
// Iterator methods
iterator begin() { return mFunctions.begin(); }
iterator end() { return mFunctions.end(); }
const_iterator begin() const { return mFunctions.begin(); }
const_iterator end() const { return mFunctions.end(); }
const_iterator cbegin() const { return mFunctions.cbegin(); }
const_iterator cend() const { return mFunctions.cend(); }
// Size information
fl::size size() const { return mFunctions.size(); }
bool empty() const { return mFunctions.empty(); }
};
template <typename... Args>
class FunctionList : public FunctionListBase<function<void(Args...)>> {
public:
void invoke(Args... args) {
for (const auto &pair : this->mFunctions) {
auto &function = pair.second;
function(args...);
}
}
};
template <>
class FunctionList<void> : public FunctionListBase<function<void()>> {
public:
void invoke() {
for (const auto &pair : this->mFunctions) {
auto &function = pair.second;
function();
}
}
};
template <>
class FunctionList<void()> : public FunctionListBase<function<void()>> {
public:
void invoke() {
for (const auto &pair : this->mFunctions) {
auto &function = pair.second;
function();
}
}
};
} // namespace fl

View File

@@ -0,0 +1,97 @@
#pragma once
#include "fl/type_traits.h"
#include "fl/utility.h"
namespace fl {
template <typename T>
class Ptr; // Forward declare Ptr to avoid header inclusion
template <typename T, typename Deleter>
class unique_ptr; // Forward declare unique_ptr to avoid header inclusion
//----------------------------------------------------------------------------
// invoke implementation - equivalent to std::invoke from C++17
//----------------------------------------------------------------------------
namespace detail {
// Helper to detect member data pointers
template <typename T>
struct is_member_data_pointer : false_type {};
template <typename T, typename C>
struct is_member_data_pointer<T C::*> : integral_constant<bool, !is_function<T>::value> {};
// Helper to detect if T is a pointer type
template <typename T>
struct is_pointer_like : false_type {};
template <typename T>
struct is_pointer_like<T*> : true_type {};
template <typename T>
struct is_pointer_like<fl::Ptr<T>> : true_type {};
template <typename T, typename Deleter>
struct is_pointer_like<fl::unique_ptr<T, Deleter>> : true_type {};
// Helper to detect if we should use pointer-to-member syntax
template <typename T>
struct use_pointer_syntax : is_pointer_like<typename remove_reference<T>::type> {};
} // namespace detail
// Main invoke function overloads
// 1a. Member function pointer with object reference
template <typename F, typename T1, typename... Args>
auto invoke(F&& f, T1&& t1, Args&&... args)
-> enable_if_t<is_member_function_pointer<typename remove_reference<F>::type>::value &&
!detail::use_pointer_syntax<T1>::value,
decltype((fl::forward<T1>(t1).*f)(fl::forward<Args>(args)...))>
{
return (fl::forward<T1>(t1).*f)(fl::forward<Args>(args)...);
}
// 1b. Member function pointer with pointer-like object
template <typename F, typename T1, typename... Args>
auto invoke(F&& f, T1&& t1, Args&&... args)
-> enable_if_t<is_member_function_pointer<typename remove_reference<F>::type>::value &&
detail::use_pointer_syntax<T1>::value,
decltype(((*fl::forward<T1>(t1)).*f)(fl::forward<Args>(args)...))>
{
return ((*fl::forward<T1>(t1)).*f)(fl::forward<Args>(args)...);
}
// 2a. Member data pointer with object reference
template <typename F, typename T1>
auto invoke(F&& f, T1&& t1)
-> enable_if_t<detail::is_member_data_pointer<typename remove_reference<F>::type>::value &&
!detail::use_pointer_syntax<T1>::value,
decltype(fl::forward<T1>(t1).*f)>
{
return fl::forward<T1>(t1).*f;
}
// 2b. Member data pointer with pointer-like object
template <typename F, typename T1>
auto invoke(F&& f, T1&& t1)
-> enable_if_t<detail::is_member_data_pointer<typename remove_reference<F>::type>::value &&
detail::use_pointer_syntax<T1>::value,
decltype((*fl::forward<T1>(t1)).*f)>
{
return (*fl::forward<T1>(t1)).*f;
}
// 3. Regular callable (function pointer, lambda, functor)
template <typename F, typename... Args>
auto invoke(F&& f, Args&&... args)
-> enable_if_t<!is_member_function_pointer<typename remove_reference<F>::type>::value &&
!detail::is_member_data_pointer<typename remove_reference<F>::type>::value,
decltype(fl::forward<F>(f)(fl::forward<Args>(args)...))>
{
return fl::forward<F>(f)(fl::forward<Args>(args)...);
}
} // namespace fl

View File

@@ -0,0 +1,8 @@
#include "fl/gamma.h"
namespace fl {
// gamma_2_8 lookup table has been moved to fl/ease.cpp.hpp
// This file is kept for compatibility with the build system
} // namespace fl

View File

@@ -0,0 +1,19 @@
#pragma once
#include "crgb.h"
#include "fl/stdint.h"
#include "fl/int.h"
#include "fl/ease.h"
namespace fl {
// Forward declaration - gamma_2_8 is now defined in fl/ease.h
extern const u16 gamma_2_8[256];
inline void gamma16(const CRGB &rgb, u16* r16, u16* g16, u16* b16) {
*r16 = gamma_2_8[rgb.r];
*g16 = gamma_2_8[rgb.g];
*b16 = gamma_2_8[rgb.b];
}
} // namespace fl

View File

@@ -0,0 +1,478 @@
#pragma once
#include "fl/int.h"
#include "fl/math.h"
#include "fl/compiler_control.h"
#include "fl/move.h"
#include "fl/compiler_control.h"
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_FLOAT_CONVERSION
FL_DISABLE_WARNING_SIGN_CONVERSION
FL_DISABLE_WARNING_IMPLICIT_INT_CONVERSION
namespace fl {
template <typename T> struct vec3 {
// value_type
using value_type = T;
T x = 0;
T y = 0;
T z = 0;
constexpr vec3() = default;
constexpr vec3(T x, T y, T z) : x(x), y(y), z(z) {}
template <typename U>
explicit constexpr vec3(U xyz) : x(xyz), y(xyz), z(xyz) {}
constexpr vec3(const vec3 &p) = default;
constexpr vec3(vec3 &&p) noexcept = default;
vec3 &operator=(vec3 &&p) noexcept = default;
vec3 &operator*=(const float &f) {
x *= f;
y *= f;
z *= f;
return *this;
}
vec3 &operator/=(const float &f) {
x /= f;
y /= f;
z /= f;
return *this;
}
vec3 &operator*=(const double &f) {
x *= f;
y *= f;
z *= f;
return *this;
}
vec3 &operator/=(const double &f) {
x /= f;
y /= f;
z /= f;
return *this;
}
vec3 &operator/=(const u16 &d) {
x /= d;
y /= d;
z /= d;
return *this;
}
vec3 &operator/=(const int &d) {
x /= d;
y /= d;
z /= d;
return *this;
}
vec3 &operator/=(const vec3 &p) {
x /= p.x;
y /= p.y;
z /= p.z;
return *this;
}
vec3 &operator+=(const vec3 &p) {
x += p.x;
y += p.y;
z += p.z;
return *this;
}
vec3 &operator-=(const vec3 &p) {
x -= p.x;
y -= p.y;
z -= p.z;
return *this;
}
vec3 &operator=(const vec3 &p) {
x = p.x;
y = p.y;
z = p.z;
return *this;
}
vec3 operator-(const vec3 &p) const {
return vec3(x - p.x, y - p.y, z - p.z);
}
vec3 operator+(const vec3 &p) const {
return vec3(x + p.x, y + p.y, z + p.z);
}
vec3 operator*(const vec3 &p) const {
return vec3(x * p.x, y * p.y, z * p.z);
}
vec3 operator/(const vec3 &p) const {
return vec3(x / p.x, y / p.y, z / p.z);
}
template <typename NumberT> vec3 operator+(const NumberT &p) const {
return vec3(x + p, y + p, z + p);
}
template <typename U> vec3 operator+(const vec3<U> &p) const {
return vec3(x + p.x, y + p.y, z + p.z);
}
template <typename NumberT> vec3 operator-(const NumberT &p) const {
return vec3(x - p, y - p, z - p);
}
template <typename NumberT> vec3 operator*(const NumberT &p) const {
return vec3(x * p, y * p, z * p);
}
template <typename NumberT> vec3 operator/(const NumberT &p) const {
T a = x / p;
T b = y / p;
T c = z / p;
return vec3<T>(a, b, c);
}
bool operator==(const vec3 &p) const {
return (x == p.x && y == p.y && z == p.z);
}
bool operator!=(const vec3 &p) const {
return (x != p.x || y != p.y || z != p.z);
}
template <typename U> bool operator==(const vec3<U> &p) const {
return (x == p.x && y == p.y && z == p.z);
}
template <typename U> bool operator!=(const vec3<U> &p) const {
return (x != p.x || y != p.y || z != p.z);
}
vec3 getMax(const vec3 &p) const {
return vec3(MAX(x, p.x), MAX(y, p.y), MAX(z, p.z));
}
vec3 getMin(const vec3 &p) const {
return vec3(MIN(x, p.x), MIN(y, p.y), MIN(z, p.z));
}
template <typename U> vec3<U> cast() const {
return vec3<U>(static_cast<U>(x), static_cast<U>(y), static_cast<U>(z));
}
template <typename U> vec3 getMax(const vec3<U> &p) const {
return vec3<U>(MAX(x, p.x), MAX(y, p.y), MAX(z, p.z));
}
template <typename U> vec3 getMin(const vec3<U> &p) const {
return vec3<U>(MIN(x, p.x), MIN(y, p.y), MIN(z, p.z));
}
T distance(const vec3 &p) const {
T dx = x - p.x;
T dy = y - p.y;
T dz = z - p.z;
return sqrt(dx * dx + dy * dy + dz * dz);
}
bool is_zero() const { return (x == 0 && y == 0 && z == 0); }
};
using vec3f = vec3<float>; // Full precision but slow.
template <typename T> struct vec2 {
// value_type
using value_type = T;
value_type x = 0;
value_type y = 0;
constexpr vec2() = default;
constexpr vec2(T x, T y) : x(x), y(y) {}
template <typename U> explicit constexpr vec2(U xy) : x(xy), y(xy) {}
constexpr vec2(const vec2 &p) = default;
constexpr vec2(vec2 &&p) noexcept = default;
vec2 &operator=(vec2 &&p) noexcept = default;
vec2 &operator*=(const float &f) {
x *= f;
y *= f;
return *this;
}
vec2 &operator/=(const float &f) {
// *this = point_xy_math::div(*this, f);
x /= f;
y /= f;
return *this;
}
vec2 &operator*=(const double &f) {
// *this = point_xy_math::mul(*this, f);
x *= f;
y *= f;
return *this;
}
vec2 &operator/=(const double &f) {
// *this = point_xy_math::div(*this, f);
x /= f;
y /= f;
return *this;
}
vec2 &operator/=(const u16 &d) {
// *this = point_xy_math::div(*this, d);
x /= d;
y /= d;
return *this;
}
vec2 &operator/=(const int &d) {
// *this = point_xy_math::div(*this, d);
x /= d;
y /= d;
return *this;
}
vec2 &operator/=(const vec2 &p) {
// *this = point_xy_math::div(*this, p);
x /= p.x;
y /= p.y;
return *this;
}
vec2 &operator+=(const vec2 &p) {
//*this = point_xy_math::add(*this, p);
x += p.x;
y += p.y;
return *this;
}
vec2 &operator-=(const vec2 &p) {
// *this = point_xy_math::sub(*this, p);
x -= p.x;
y -= p.y;
return *this;
}
vec2 &operator=(const vec2 &p) {
x = p.x;
y = p.y;
return *this;
}
vec2 operator-(const vec2 &p) const { return vec2(x - p.x, y - p.y); }
vec2 operator+(const vec2 &p) const { return vec2(x + p.x, y + p.y); }
vec2 operator*(const vec2 &p) const { return vec2(x * p.x, y * p.y); }
vec2 operator/(const vec2 &p) const { return vec2(x / p.x, y / p.y); }
template <typename NumberT> vec2 operator+(const NumberT &p) const {
return vec2(x + p, y + p);
}
template <typename U> vec2 operator+(const vec2<U> &p) const {
return vec2(x + p.x, y + p.x);
}
template <typename NumberT> vec2 operator-(const NumberT &p) const {
return vec2(x - p, y - p);
}
template <typename NumberT> vec2 operator*(const NumberT &p) const {
return vec2(x * p, y * p);
}
template <typename NumberT> vec2 operator/(const NumberT &p) const {
T a = x / p;
T b = y / p;
return vec2<T>(a, b);
}
bool operator==(const vec2 &p) const { return (x == p.x && y == p.y); }
bool operator!=(const vec2 &p) const { return (x != p.x || y != p.y); }
template <typename U> bool operator==(const vec2<U> &p) const {
return (x == p.x && y == p.y);
}
template <typename U> bool operator!=(const vec2<U> &p) const {
return (x != p.x || y != p.y);
}
vec2 getMax(const vec2 &p) const { return vec2(MAX(x, p.x), MAX(y, p.y)); }
vec2 getMin(const vec2 &p) const { return vec2(MIN(x, p.x), MIN(y, p.y)); }
template <typename U> vec2<U> cast() const {
return vec2<U>(static_cast<U>(x), static_cast<U>(y));
}
template <typename U> vec2 getMax(const vec2<U> &p) const {
return vec2<U>(MAX(x, p.x), MAX(y, p.y));
}
template <typename U> vec2 getMin(const vec2<U> &p) const {
return vec2<U>(MIN(x, p.x), MIN(y, p.y));
}
T distance(const vec2 &p) const {
T dx = x - p.x;
T dy = y - p.y;
return sqrt(dx * dx + dy * dy);
}
bool is_zero() const { return (x == 0 && y == 0); }
};
using vec2f = vec2<float>; // Full precision but slow.
using vec2u8 = vec2<fl::u8>; // 8-bit unsigned integer vector.
using vec2i16 = vec2<i16>; // 16-bit signed integer vector.
// Legacy support for vec3
using pair_xyz_float = vec3<float>; // Legacy name for vec3f
// Legacy support for vec2
using pair_xy_float = vec2<float>; // Legacy name for vec2f
// pair_xy<T> is the legacy name for vec2<T>
template <typename T> struct pair_xy : public vec2<T> {
using value_type = T;
using vec2<T>::vec2;
pair_xy() = default;
pair_xy(const vec2<T> &p) : vec2<T>(p) {}
};
template <typename T> struct line_xy {
vec2<T> start;
vec2<T> end;
line_xy() = default;
line_xy(const vec2<T> &start, const vec2<T> &end)
: start(start), end(end) {}
line_xy(T start_x, T start_y, T end_x, T end_y)
: start(start_x, start_y), end(end_x, end_y) {}
line_xy(const line_xy &other) = default;
line_xy &operator=(const line_xy &other) = default;
line_xy(line_xy &&other) noexcept = default;
line_xy &operator=(line_xy &&other) noexcept = default;
bool empty() const { return (start == end); }
float distance_to(const vec2<T> &p,
vec2<T> *out_projected = nullptr) const {
return distance_to_line_with_point(p, start, end, out_projected);
}
private:
// Computes the closest distance from `p` to the line through `a` and `b`,
// and writes the projected point.
static float distance_to_line_with_point(vec2<T> p, vec2<T> a, vec2<T> b,
vec2<T> *out_projected) {
vec2<T> maybe;
vec2<T> &out_proj = out_projected ? *out_projected : maybe;
float dx = b.x - a.x;
float dy = b.y - a.y;
float len_sq = dx * dx + dy * dy;
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING(float-equal)
const bool is_zero = (len_sq == 0.0f);
FL_DISABLE_WARNING_POP
if (is_zero) {
// a == b, the segment is a point
out_proj = a;
dx = p.x - a.x;
dy = p.y - a.y;
return sqrt(dx * dx + dy * dy);
}
// Project point p onto the line segment, computing parameter t
float t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len_sq;
// Clamp t to [0,1] to stay within the segment
if (t < 0.0f)
t = 0.0f;
else if (t > 1.0f)
t = 1.0f;
// Find the closest point
out_proj.x = a.x + t * dx;
out_proj.y = a.y + t * dy;
dx = p.x - out_proj.x;
dy = p.y - out_proj.y;
return sqrt(dx * dx + dy * dy);
}
};
template <typename T> struct rect {
vec2<T> mMin;
vec2<T> mMax;
rect() = default;
rect(const vec2<T> &min, const vec2<T> &max) : mMin(min), mMax(max) {}
rect(T min_x, T min_y, T max_x, T max_y)
: mMin(min_x, min_y), mMax(max_x, max_y) {}
rect(const rect &other) = default;
rect &operator=(const rect &other) = default;
rect(rect &&other) noexcept = default;
rect &operator=(rect &&other) noexcept = default;
u16 width() const { return mMax.x - mMin.x; }
u16 height() const { return mMax.y - mMin.y; }
bool empty() const { return (mMin.x == mMax.x && mMin.y == mMax.y); }
void expand(const vec2<T> &p) { expand(p.x, p.y); }
void expand(const rect &r) {
expand(r.mMin);
expand(r.mMax);
}
void expand(T x, T y) {
mMin.x = MIN(mMin.x, x);
mMin.y = MIN(mMin.y, y);
mMax.x = MAX(mMax.x, x);
mMax.y = MAX(mMax.y, y);
}
bool contains(const vec2<T> &p) const {
return (p.x >= mMin.x && p.x < mMax.x && p.y >= mMin.y && p.y < mMax.y);
}
bool contains(const T &x, const T &y) const {
return contains(vec2<T>(x, y));
}
bool operator==(const rect &r) const {
return (mMin == r.mMin && mMax == r.mMax);
}
bool operator!=(const rect &r) const { return !(*this == r); }
template <typename U> bool operator==(const rect<U> &r) const {
return (mMin == r.mMin && mMax == r.mMax);
}
template <typename U> bool operator!=(const rect<U> &r) const {
return !(*this == r);
}
};
} // namespace fl
FL_DISABLE_WARNING_POP

View File

@@ -0,0 +1,147 @@
#include "fl/gradient.h"
#include "fl/assert.h"
#include "fl/colorutils.h"
namespace fl {
namespace {
struct Visitor {
Visitor(u8 index) : index(index) {}
void accept(const CRGBPalette16 *palette) {
CRGB c = ColorFromPalette(*palette, index);
return_val = c;
}
void accept(const CRGBPalette32 *palette) {
CRGB c = ColorFromPalette(*palette, index);
return_val = c;
}
void accept(const CRGBPalette256 *palette) {
CRGB c = ColorFromPaletteExtended(*palette, index);
return_val = c;
}
void accept(const Gradient::GradientFunction &func) {
CRGB c = func(index);
return_val = c;
}
template <typename T> void accept(const T &obj) {
// This should never be called, but we need to provide a default
// implementation to avoid compilation errors.
accept(&obj);
}
CRGB return_val;
u8 index;
};
struct VisitorFill {
VisitorFill(span<const u8> indices, span<CRGB> output)
: output(output), indices(indices) {
// This assert was triggering on the corkscrew example. Not sure why
// but the corrective action of taking the min was corrective action.
// FASTLED_ASSERT(
// indices.size() == output.size(),
// "Gradient::fill: indices and output must be the same size"
// "\nSize was" << indices.size() << " and " << output.size());
n = MIN(indices.size(), output.size());
}
void accept(const CRGBPalette16 *palette) {
for (fl::size i = 0; i < n; ++i) {
output[i] = ColorFromPalette(*palette, indices[i]);
}
}
void accept(const CRGBPalette32 *palette) {
for (fl::size i = 0; i < n; ++i) {
output[i] = ColorFromPalette(*palette, indices[i]);
}
}
void accept(const CRGBPalette256 *palette) {
for (fl::size i = 0; i < n; ++i) {
output[i] = ColorFromPaletteExtended(*palette, indices[i]);
}
}
void accept(const Gradient::GradientFunction &func) {
for (fl::size i = 0; i < n; ++i) {
output[i] = func(indices[i]);
}
}
template <typename T> void accept(const T &obj) {
// This should never be called, but we need to provide a default
// implementation to avoid compilation errors.
accept(&obj);
}
span<CRGB> output;
span<const u8> indices;
u8 n = 0;
};
} // namespace
CRGB Gradient::colorAt(u8 index) const {
Visitor visitor(index);
mVariant.visit(visitor);
return visitor.return_val;
}
template <typename T> Gradient::Gradient(T *palette) { set(palette); }
Gradient::Gradient(const Gradient &other) : mVariant(other.mVariant) {}
Gradient::Gradient(Gradient &&other) noexcept
: mVariant(move(other.mVariant)) {}
void Gradient::set(const CRGBPalette32 *palette) { mVariant = palette; }
void Gradient::set(const CRGBPalette256 *palette) { mVariant = palette; }
void Gradient::set(const CRGBPalette16 *palette) { mVariant = palette; }
void Gradient::set(const GradientFunction &func) { mVariant = func; }
Gradient &Gradient::operator=(const Gradient &other) {
if (this != &other) {
mVariant = other.mVariant;
}
return *this;
}
void Gradient::fill(span<const u8> input, span<CRGB> output) const {
VisitorFill visitor(input, output);
mVariant.visit(visitor);
}
CRGB GradientInlined::colorAt(u8 index) const {
Visitor visitor(index);
mVariant.visit(visitor);
return visitor.return_val;
}
void GradientInlined::fill(span<const u8> input,
span<CRGB> output) const {
VisitorFill visitor(input, output);
mVariant.visit(visitor);
}
Gradient::Gradient(const GradientInlined &other) {
// Visitor is cumbersome but guarantees all paths are handled.
struct Copy {
Copy(Gradient &owner) : mOwner(owner) {}
void accept(const CRGBPalette16 &palette) { mOwner.set(&palette); }
void accept(const CRGBPalette32 &palette) { mOwner.set(&palette); }
void accept(const CRGBPalette256 &palette) { mOwner.set(&palette); }
void accept(const GradientFunction &func) { mOwner.set(func); }
Gradient &mOwner;
};
Copy copy_to_self(*this);
other.variant().visit(copy_to_self);
}
} // namespace fl

View File

@@ -0,0 +1,71 @@
#pragma once
#include "fl/colorutils.h"
#include "fl/function.h"
#include "fl/span.h"
#include "fl/type_traits.h"
#include "fl/variant.h"
namespace fl {
class CRGBPalette16;
class CRGBPalette32;
class CRGBPalette256;
class GradientInlined;
class Gradient {
public:
using GradientFunction = fl::function<CRGB(u8 index)>;
Gradient() = default;
Gradient(const GradientInlined &other);
template <typename T> Gradient(T *palette);
Gradient(const Gradient &other);
Gradient &operator=(const Gradient &other);
Gradient(Gradient &&other) noexcept;
// non template allows carefull control of what can be set.
void set(const CRGBPalette16 *palette);
void set(const CRGBPalette32 *palette);
void set(const CRGBPalette256 *palette);
void set(const GradientFunction &func);
CRGB colorAt(u8 index) const;
void fill(span<const u8> input, span<CRGB> output) const;
private:
using GradientVariant =
Variant<const CRGBPalette16 *, const CRGBPalette32 *,
const CRGBPalette256 *, GradientFunction>;
GradientVariant mVariant;
};
class GradientInlined {
public:
using GradientFunction = fl::function<CRGB(u8 index)>;
using GradientVariant =
Variant<CRGBPalette16, CRGBPalette32, CRGBPalette256, GradientFunction>;
GradientInlined() = default;
template <typename T> GradientInlined(const T &palette) { set(palette); }
GradientInlined(const GradientInlined &other) = default;
GradientInlined &operator=(const GradientInlined &other) = default;
void set(const CRGBPalette16 &palette) { mVariant = palette; }
void set(const CRGBPalette32 &palette) { mVariant = palette; }
void set(const CRGBPalette256 &palette) { mVariant = palette; }
void set(const GradientFunction &func) { mVariant = func; }
CRGB colorAt(u8 index) const;
void fill(span<const u8> input, span<CRGB> output) const;
GradientVariant &variant() { return mVariant; }
const GradientVariant &variant() const { return mVariant; }
private:
GradientVariant mVariant;
};
} // namespace fl

View File

@@ -0,0 +1,92 @@
#pragma once
#include "fl/span.h"
#include "fl/vector.h"
#include "fl/allocator.h"
namespace fl {
template <typename T> class Grid {
public:
Grid() = default;
Grid(u32 width, u32 height) { reset(width, height); }
void reset(u32 width, u32 height) {
clear();
if (width != mWidth || height != mHeight) {
mWidth = width;
mHeight = height;
mData.resize(width * height);
}
mSlice = fl::MatrixSlice<T>(mData.data(), width, height, 0, 0,
width, height);
}
void clear() {
for (u32 i = 0; i < mWidth * mHeight; ++i) {
mData[i] = T();
}
}
vec2<T> minMax() const {
T minValue = mData[0];
T maxValue = mData[0];
for (u32 i = 1; i < mWidth * mHeight; ++i) {
if (mData[i] < minValue) {
minValue = mData[i];
}
if (mData[i] > maxValue) {
maxValue = mData[i];
}
}
// *min = minValue;
// *max = maxValue;
vec2<T> out(minValue, maxValue);
return out;
}
T &at(u32 x, u32 y) { return access(x, y); }
const T &at(u32 x, u32 y) const { return access(x, y); }
T &operator()(u32 x, u32 y) { return at(x, y); }
const T &operator()(u32 x, u32 y) const { return at(x, y); }
u32 width() const { return mWidth; }
u32 height() const { return mHeight; }
T* data() { return mData.data(); }
const T* data() const { return mData.data(); }
fl::size size() const { return mData.size(); }
private:
static T &NullValue() {
static T gNull;
return gNull;
}
T &access(u32 x, u32 y) {
if (x < mWidth && y < mHeight) {
return mSlice.at(x, y);
} else {
return NullValue(); // safe.
}
}
const T &access(u32 x, u32 y) const {
if (x < mWidth && y < mHeight) {
return mSlice.at(x, y);
} else {
return NullValue(); // safe.
}
}
fl::vector<T, fl::allocator_psram<T>> mData;
u32 mWidth = 0;
u32 mHeight = 0;
fl::MatrixSlice<T> mSlice;
};
} // namespace fl

View File

@@ -0,0 +1,7 @@
#pragma once
#ifndef __has_include
#define FL_HAS_INCLUDE(x) 0
#else
#define FL_HAS_INCLUDE(x) __has_include(x)
#endif

View File

@@ -0,0 +1,264 @@
#pragma once
#include "fl/str.h"
#include "fl/type_traits.h"
#include "fl/int.h"
#include "fl/stdint.h"
#include "fl/force_inline.h"
#include "fl/memfill.h"
#include <string.h>
#include "fl/compiler_control.h"
namespace fl {
template <typename T> struct vec2;
//-----------------------------------------------------------------------------
// MurmurHash3 x86 32-bit
//-----------------------------------------------------------------------------
// Based on the publicdomain implementation by Austin Appleby:
// https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp
static inline u32 MurmurHash3_x86_32(const void *key, fl::size len,
u32 seed = 0) {
FL_DISABLE_WARNING_PUSH;
FL_DISABLE_WARNING_IMPLICIT_FALLTHROUGH;
const fl::u8 *data = static_cast<const fl::u8 *>(key);
const int nblocks = int(len / 4);
u32 h1 = seed;
const u32 c1 = 0xcc9e2d51;
const u32 c2 = 0x1b873593;
// body
const u32 *blocks = reinterpret_cast<const u32 *>(data);
for (int i = 0; i < nblocks; ++i) {
u32 k1 = blocks[i];
k1 *= c1;
k1 = (k1 << 15) | (k1 >> 17);
k1 *= c2;
h1 ^= k1;
h1 = (h1 << 13) | (h1 >> 19);
h1 = h1 * 5 + 0xe6546b64;
}
// tail
const fl::u8 *tail = data + (nblocks * 4);
u32 k1 = 0;
switch (len & 3) {
case 3:
k1 ^= u32(tail[2]) << 16;
case 2:
k1 ^= u32(tail[1]) << 8;
case 1:
k1 ^= u32(tail[0]);
k1 *= c1;
k1 = (k1 << 15) | (k1 >> 17);
k1 *= c2;
h1 ^= k1;
}
// finalization
h1 ^= u32(len);
// fmix32
h1 ^= h1 >> 16;
h1 *= 0x85ebca6b;
h1 ^= h1 >> 13;
h1 *= 0xc2b2ae35;
h1 ^= h1 >> 16;
return h1;
FL_DISABLE_WARNING_POP;
}
//-----------------------------------------------------------------------------
// Fast, cheap 32-bit integer hash (Thomas Wang)
//-----------------------------------------------------------------------------
static inline u32 fast_hash32(u32 x) noexcept {
x = (x ^ 61u) ^ (x >> 16);
x = x + (x << 3);
x = x ^ (x >> 4);
x = x * 0x27d4eb2dU;
x = x ^ (x >> 15);
return x;
}
// 3) Handy two-word hasher
static inline u32 hash_pair(u32 a, u32 b,
u32 seed = 0) noexcept {
// mix in 'a', then mix in 'b'
u32 h = fast_hash32(seed ^ a);
return fast_hash32(h ^ b);
}
static inline u32 fast_hash64(u64 x) noexcept {
u32 x1 = static_cast<u32>(x & 0x00000000FFFFFFFF);
u32 x2 = static_cast<u32>(x >> 32);
return hash_pair(x1, x2);
}
//-----------------------------------------------------------------------------
// Functor for hashing arbitrary byteranges to a 32bit value
//-----------------------------------------------------------------------------
// https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp
template <typename T> struct Hash {
static_assert(fl::is_pod<T>::value,
"fl::Hash<T> only supports POD types (integrals, floats, "
"etc.), you need to define your own hash.");
u32 operator()(const T &key) const noexcept {
return MurmurHash3_x86_32(&key, sizeof(T));
}
};
template <typename T> struct FastHash {
static_assert(fl::is_pod<T>::value,
"fl::FastHash<T> only supports POD types (integrals, floats, "
"etc.), you need to define your own hash.");
u32 operator()(const T &key) const noexcept {
return fast_hash32(key);
}
};
template <typename T> struct FastHash<vec2<T>> {
u32 operator()(const vec2<T> &key) const noexcept {
if (sizeof(T) == sizeof(fl::u8)) {
u32 x = static_cast<u32>(key.x) +
(static_cast<u32>(key.y) << 8);
return fast_hash32(x);
}
if (sizeof(T) == sizeof(u16)) {
u32 x = static_cast<u32>(key.x) +
(static_cast<u32>(key.y) << 16);
return fast_hash32(x);
}
if (sizeof(T) == sizeof(u32)) {
return hash_pair(key.x, key.y);
}
return MurmurHash3_x86_32(&key, sizeof(T));
}
};
template <typename T> struct Hash<T *> {
u32 operator()(T *key) const noexcept {
if (sizeof(T *) == sizeof(u32)) {
u32 key_u = reinterpret_cast<fl::uptr>(key);
return fast_hash32(key_u);
} else {
return MurmurHash3_x86_32(key, sizeof(T *));
}
}
};
template <typename T> struct Hash<vec2<T>> {
u32 operator()(const vec2<T> &key) const noexcept {
#ifndef __clang__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
T packed[2] = {key.x, key.y};
const void *p = &packed[0];
return MurmurHash3_x86_32(p, sizeof(packed));
#ifndef __clang__
#pragma GCC diagnostic pop
#endif
}
};
template <typename T> struct Hash<fl::shared_ptr<T>> {
u32 operator()(const T &key) const noexcept {
auto hasher = Hash<T *>();
return hasher(key.get());
}
};
template <typename T> struct Hash<fl::WeakPtr<T>> {
u32 operator()(const fl::WeakPtr<T> &key) const noexcept {
fl::uptr val = key.ptr_value();
return MurmurHash3_x86_32(&val, sizeof(fl::uptr));
}
};
template<> struct Hash<float> {
u32 operator()(const float key) const noexcept {
u32 ikey = fl::bit_cast<u32>(key);
return fast_hash32(ikey);
}
};
template<> struct Hash<double> {
u32 operator()(const double& key) const noexcept {
return MurmurHash3_x86_32(&key, sizeof(double));
}
};
template<> struct Hash<i32> {
u32 operator()(const i32 key) const noexcept {
u32 ukey = static_cast<u32>(key);
return fast_hash32(ukey);
}
};
template<> struct Hash<bool> {
u32 operator()(const bool key) const noexcept {
return fast_hash32(key);
}
};
template<> struct Hash<fl::u8> {
u32 operator()(const fl::u8 &key) const noexcept {
return fast_hash32(key);
}
};
template<> struct Hash<u16> {
u32 operator()(const u16 &key) const noexcept {
return fast_hash32(key);
}
};
template<> struct Hash<u32> {
u32 operator()(const u32 &key) const noexcept {
return fast_hash32(key);
}
};
template<> struct Hash<i8> {
u32 operator()(const i8 &key) const noexcept {
u8 v = static_cast<u8>(key);
return fast_hash32(v);
}
};
template<> struct Hash<i16> {
u32 operator()(const i16 &key) const noexcept {
u16 ukey = static_cast<u16>(key);
return fast_hash32(ukey);
}
};
// FASTLED_DEFINE_FAST_HASH(fl::u8)
// FASTLED_DEFINE_FAST_HASH(u16)
// FASTLED_DEFINE_FAST_HASH(u32)
// FASTLED_DEFINE_FAST_HASH(i8)
// FASTLED_DEFINE_FAST_HASH(i16)
// FASTLED_DEFINE_FAST_HASH(bool)
// FASTLED_DEFINE_FAST_HASH(int)
//-----------------------------------------------------------------------------
// Convenience for std::string → u32
//----------------------------------------------------------------------------
template <> struct Hash<fl::string> {
u32 operator()(const fl::string &key) const noexcept {
return MurmurHash3_x86_32(key.data(), key.size());
}
};
} // namespace fl

View File

@@ -0,0 +1,725 @@
#pragma once
/*
HashMap that is optimized for embedded devices. The hashmap
will store upto N elements inline, and will spill over to a heap.
This hashmap will try not to grow by detecting during rehash that
the number of tombstones is greater than the number of elements.
This will keep the memory from growing during multiple inserts
and removals.
*/
// #include <cstddef>
// #include <iterator>
#include "fl/assert.h"
#include "fl/bitset.h"
#include "fl/clamp.h"
#include "fl/hash.h"
#include "fl/map_range.h"
#include "fl/optional.h"
#include "fl/pair.h"
#include "fl/type_traits.h"
#include "fl/vector.h"
#include "fl/warn.h"
#include "fl/align.h"
#include "fl/compiler_control.h"
#include "fl/math_macros.h"
#include "fl/compiler_control.h"
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_SHORTEN_64_TO_32
namespace fl {
// // begin using declarations for stl compatibility
// use fl::equal_to;
// use fl::hash_map;
// use fl::unordered_map;
// // end using declarations for stl compatibility
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
template <typename T> struct EqualTo {
bool operator()(const T &a, const T &b) const { return a == b; }
};
FL_DISABLE_WARNING_POP
// -- HashMap class
// ------------------------------------------------------------- Begin HashMap
// class
#ifndef FASTLED_HASHMAP_INLINED_COUNT
#define FASTLED_HASHMAP_INLINED_COUNT 8
#endif
template <typename Key, typename T, typename Hash = Hash<Key>,
typename KeyEqual = EqualTo<Key>,
int INLINED_COUNT = FASTLED_HASHMAP_INLINED_COUNT>
class FL_ALIGN HashMap {
public:
HashMap() : HashMap(FASTLED_HASHMAP_INLINED_COUNT, 0.7f) {}
HashMap(fl::size initial_capacity) : HashMap(initial_capacity, 0.7f) {}
HashMap(fl::size initial_capacity, float max_load)
: _buckets(next_power_of_two(initial_capacity)), _size(0),
_tombstones(0), _occupied(next_power_of_two(initial_capacity)),
_deleted(next_power_of_two(initial_capacity)) {
setLoadFactor(max_load);
}
void setLoadFactor(float f) {
f = fl::clamp(f, 0.f, 1.f);
mLoadFactor = fl::map_range<float, u8>(f, 0.f, 1.f, 0, 255);
}
// Iterator support.
struct iterator {
// Standard iterator typedefs
// using difference_type = std::ptrdiff_t;
using value_type = pair<const Key, T>; // Keep const Key as per standard
using pointer = value_type *;
using reference = value_type &;
// using iterator_category = std::forward_iterator_tag;
iterator() : _map(nullptr), _idx(0) {}
iterator(HashMap *m, fl::size idx) : _map(m), _idx(idx) {
advance_to_occupied();
}
value_type operator*() const {
auto &e = _map->_buckets[_idx];
return {e.key, e.value};
}
pointer operator->() {
// Use reinterpret_cast since pair<const Key, T> and pair<Key, T> are different types
// but have the same memory layout, then destroy/reconstruct to avoid assignment issues
using mutable_value_type = pair<Key, T>;
auto& mutable_cached = *reinterpret_cast<mutable_value_type*>(&_cached_value);
mutable_cached.~mutable_value_type();
new (&mutable_cached) mutable_value_type(operator*());
return &_cached_value;
}
pointer operator->() const {
// Use reinterpret_cast since pair<const Key, T> and pair<Key, T> are different types
// but have the same memory layout, then destroy/reconstruct to avoid assignment issues
using mutable_value_type = pair<Key, T>;
auto& mutable_cached = *fl::bit_cast<mutable_value_type*>(&_cached_value);
mutable_cached.~mutable_value_type();
new (&mutable_cached) mutable_value_type(operator*());
return &_cached_value;
}
iterator &operator++() {
++_idx;
advance_to_occupied();
return *this;
}
iterator operator++(int) {
iterator tmp = *this;
++(*this);
return tmp;
}
bool operator==(const iterator &o) const {
return _map == o._map && _idx == o._idx;
}
bool operator!=(const iterator &o) const { return !(*this == o); }
void advance_to_occupied() {
if (!_map)
return;
fl::size cap = _map->_buckets.size();
while (_idx < cap && !_map->is_occupied(_idx))
++_idx;
}
private:
HashMap *_map;
fl::size _idx;
mutable value_type _cached_value;
friend class HashMap;
};
struct const_iterator {
// Standard iterator typedefs
// using difference_type = std::ptrdiff_t;
using value_type = pair<const Key, T>;
using pointer = const value_type *;
using reference = const value_type &;
// using iterator_category = std::forward_iterator_tag;
const_iterator() : _map(nullptr), _idx(0) {}
const_iterator(const HashMap *m, fl::size idx) : _map(m), _idx(idx) {
advance_to_occupied();
}
const_iterator(const iterator &it) : _map(it._map), _idx(it._idx) {}
value_type operator*() const {
auto &e = _map->_buckets[_idx];
return {e.key, e.value};
}
pointer operator->() const {
// Use reinterpret_cast since pair<const Key, T> and pair<Key, T> are different types
// but have the same memory layout, then destroy/reconstruct to avoid assignment issues
using mutable_value_type = pair<Key, T>;
auto& mutable_cached = *reinterpret_cast<mutable_value_type*>(&_cached_value);
mutable_cached.~mutable_value_type();
new (&mutable_cached) mutable_value_type(operator*());
return &_cached_value;
}
const_iterator &operator++() {
++_idx;
advance_to_occupied();
return *this;
}
const_iterator operator++(int) {
const_iterator tmp = *this;
++(*this);
return tmp;
}
bool operator==(const const_iterator &o) const {
return _map == o._map && _idx == o._idx;
}
bool operator!=(const const_iterator &o) const { return !(*this == o); }
void advance_to_occupied() {
if (!_map)
return;
fl::size cap = _map->_buckets.size();
while (_idx < cap && !_map->is_occupied(_idx))
++_idx;
}
friend class HashMap;
private:
const HashMap *_map;
fl::size _idx;
mutable value_type _cached_value;
};
iterator begin() { return iterator(this, 0); }
iterator end() { return iterator(this, _buckets.size()); }
const_iterator begin() const { return const_iterator(this, 0); }
const_iterator end() const { return const_iterator(this, _buckets.size()); }
const_iterator cbegin() const { return const_iterator(this, 0); }
const_iterator cend() const {
return const_iterator(this, _buckets.size());
}
static bool NeedsRehash(fl::size size, fl::size bucket_size, fl::size tombstones,
u8 load_factor) {
// (size + tombstones) << 8 : multiply numerator by 256
// capacity * max_load : denominator * threshold
u32 lhs = (size + tombstones) << 8;
u32 rhs = (bucket_size * load_factor);
return lhs > rhs;
}
// returns true if (size + tombs)/capacity > _max_load/256
bool needs_rehash() const {
return NeedsRehash(_size, _buckets.size(), _tombstones, mLoadFactor);
}
// insert or overwrite
void insert(const Key &key, const T &value) {
const bool will_rehash = needs_rehash();
if (will_rehash) {
// if half the buckets are tombstones, rehash inline to prevent
// memory spill over into the heap.
if (_tombstones > _size) {
rehash_inline_no_resize();
} else {
rehash(_buckets.size() * 2);
}
}
fl::size idx;
bool is_new;
fl::pair<fl::size, bool> p = find_slot(key);
idx = p.first;
is_new = p.second;
if (is_new) {
_buckets[idx].key = key;
_buckets[idx].value = value;
mark_occupied(idx);
++_size;
} else {
FASTLED_ASSERT(idx != npos(), "HashMap::insert: invalid index at "
<< idx << " which is " << npos());
_buckets[idx].value = value;
}
}
// Move version of insert
void insert(Key &&key, T &&value) {
const bool will_rehash = needs_rehash();
if (will_rehash) {
// if half the buckets are tombstones, rehash inline to prevent
// memory spill over into the heap.
if (_tombstones > _size) {
rehash_inline_no_resize();
} else {
rehash(_buckets.size() * 2);
}
}
fl::size idx;
bool is_new;
fl::pair<fl::size, bool> p = find_slot(key);
idx = p.first;
is_new = p.second;
if (is_new) {
_buckets[idx].key = fl::move(key);
_buckets[idx].value = fl::move(value);
mark_occupied(idx);
++_size;
} else {
FASTLED_ASSERT(idx != npos(), "HashMap::insert: invalid index at "
<< idx << " which is " << npos());
_buckets[idx].value = fl::move(value);
}
}
// remove key; returns true if removed
bool remove(const Key &key) {
auto idx = find_index(key);
if (idx == npos())
return false;
mark_deleted(idx);
--_size;
++_tombstones;
return true;
}
bool erase(const Key &key) { return remove(key); }
// Iterator-based erase - more efficient when you already have the iterator position
iterator erase(iterator it) {
if (it == end() || it._map != this) {
return end(); // Invalid iterator
}
// Mark the current position as deleted
mark_deleted(it._idx);
--_size;
++_tombstones;
// Advance to next valid element and return iterator to it
++it._idx;
it.advance_to_occupied();
return it;
}
void clear() {
_buckets.assign(_buckets.size(), Entry{});
_occupied.reset();
_deleted.reset();
_size = _tombstones = 0;
}
// find pointer to value or nullptr
T *find_value(const Key &key) {
auto idx = find_index(key);
return idx == npos() ? nullptr : &_buckets[idx].value;
}
const T *find_value(const Key &key) const {
auto idx = find_index(key);
return idx == npos() ? nullptr : &_buckets[idx].value;
}
iterator find(const Key &key) {
auto idx = find_index(key);
return idx == npos() ? end() : iterator(this, idx);
}
const_iterator find(const Key &key) const {
auto idx = find_index(key);
return idx == npos() ? end() : const_iterator(this, idx);
}
bool contains(const Key &key) const {
auto idx = find_index(key);
return idx != npos();
}
// access or default-construct
T &operator[](const Key &key) {
fl::size idx;
bool is_new;
fl::pair<fl::size, bool> p = find_slot(key);
idx = p.first;
is_new = p.second;
// Check if find_slot failed to find a valid slot (HashMap is full)
if (idx == npos()) {
// Need to resize to make room
if (needs_rehash()) {
// if half the buckets are tombstones, rehash inline to prevent
// memory growth. Otherwise, double the size.
if (_tombstones >= _buckets.size() / 2) {
rehash_inline_no_resize();
} else {
rehash(_buckets.size() * 2);
}
} else {
// Force a rehash with double size if needs_rehash() doesn't detect the issue
rehash(_buckets.size() * 2);
}
// Try find_slot again after resize
p = find_slot(key);
idx = p.first;
is_new = p.second;
// If still npos() after resize, something is seriously wrong
if (idx == npos()) {
// This should never happen after a successful resize
static T default_value{};
return default_value;
}
}
if (is_new) {
_buckets[idx].key = key;
_buckets[idx].value = T{};
mark_occupied(idx);
++_size;
}
return _buckets[idx].value;
}
fl::size size() const { return _size; }
bool empty() const { return _size == 0; }
fl::size capacity() const { return _buckets.size(); }
private:
static fl::size npos() {
return static_cast<fl::size>(-1);
}
// Helper methods to check entry state
bool is_occupied(fl::size idx) const { return _occupied.test(idx); }
bool is_deleted(fl::size idx) const { return _deleted.test(idx); }
bool is_empty(fl::size idx) const {
return !is_occupied(idx) && !is_deleted(idx);
}
void mark_occupied(fl::size idx) {
_occupied.set(idx);
_deleted.reset(idx);
}
void mark_deleted(fl::size idx) {
_occupied.reset(idx);
_deleted.set(idx);
}
void mark_empty(fl::size idx) {
_occupied.reset(idx);
_deleted.reset(idx);
}
struct alignas(fl::max_align<Key, T>::value) Entry {
Key key;
T value;
void swap(Entry &other) {
fl::swap(key, other.key);
fl::swap(value, other.value);
}
};
static fl::size next_power_of_two(fl::size n) {
fl::size p = 1;
while (p < n)
p <<= 1;
return p;
}
pair<fl::size, bool> find_slot(const Key &key) const {
const fl::size cap = _buckets.size();
const fl::size mask = cap - 1;
const fl::size h = _hash(key) & mask;
fl::size first_tomb = npos();
if (cap <= 8) {
// linear probing
for (fl::size i = 0; i < cap; ++i) {
const fl::size idx = (h + i) & mask;
if (is_empty(idx))
return {first_tomb != npos() ? first_tomb : idx, true};
if (is_deleted(idx)) {
if (first_tomb == npos())
first_tomb = idx;
} else if (is_occupied(idx) && _equal(_buckets[idx].key, key)) {
return {idx, false};
}
}
} else {
// quadratic probing up to 8 tries
fl::size i = 0;
for (; i < 8; ++i) {
const fl::size idx = (h + i + i * i) & mask;
if (is_empty(idx))
return {first_tomb != npos() ? first_tomb : idx, true};
if (is_deleted(idx)) {
if (first_tomb == npos())
first_tomb = idx;
} else if (is_occupied(idx) && _equal(_buckets[idx].key, key)) {
return {idx, false};
}
}
// fallback to linear for the rest
for (; i < cap; ++i) {
const fl::size idx = (h + i) & mask;
if (is_empty(idx))
return {first_tomb != npos() ? first_tomb : idx, true};
if (is_deleted(idx)) {
if (first_tomb == npos())
first_tomb = idx;
} else if (is_occupied(idx) && _equal(_buckets[idx].key, key)) {
return {idx, false};
}
}
}
return {npos(), false};
}
enum {
kLinearProbingOnlySize = 8,
kQuadraticProbingTries = 8,
};
fl::size find_index(const Key &key) const {
const fl::size cap = _buckets.size();
const fl::size mask = cap - 1;
const fl::size h = _hash(key) & mask;
if (cap <= kLinearProbingOnlySize) {
// linear probing
for (fl::size i = 0; i < cap; ++i) {
const fl::size idx = (h + i) & mask;
if (is_empty(idx))
return npos();
if (is_occupied(idx) && _equal(_buckets[idx].key, key))
return idx;
}
} else {
// quadratic probing up to 8 tries
fl::size i = 0;
for (; i < kQuadraticProbingTries; ++i) {
const fl::size idx = (h + i + i * i) & mask;
if (is_empty(idx))
return npos();
if (is_occupied(idx) && _equal(_buckets[idx].key, key))
return idx;
}
// fallback to linear for the rest
for (; i < cap; ++i) {
const fl::size idx = (h + i) & mask;
if (is_empty(idx))
return npos();
if (is_occupied(idx) && _equal(_buckets[idx].key, key))
return idx;
}
}
return npos();
}
fl::size find_unoccupied_index_using_bitset(
const Key &key, const fl::bitset<1024> &occupied_set) const {
const fl::size cap = _buckets.size();
const fl::size mask = cap - 1;
const fl::size h = _hash(key) & mask;
if (cap <= kLinearProbingOnlySize) {
// linear probing
for (fl::size i = 0; i < cap; ++i) {
const fl::size idx = (h + i) & mask;
bool occupied = occupied_set.test(idx);
if (occupied) {
continue;
}
return idx;
}
} else {
// quadratic probing up to 8 tries
fl::size i = 0;
for (; i < kQuadraticProbingTries; ++i) {
const fl::size idx = (h + i + i * i) & mask;
bool occupied = occupied_set.test(idx);
if (occupied) {
continue;
}
return idx;
}
// fallback to linear for the rest
for (; i < cap; ++i) {
const fl::size idx = (h + i) & mask;
bool occupied = occupied_set.test(idx);
if (occupied) {
continue;
}
return idx;
}
}
return npos();
}
void rehash(fl::size new_cap) {
new_cap = next_power_of_two(new_cap);
fl::vector_inlined<Entry, INLINED_COUNT> old;
fl::bitset<1024> old_occupied = _occupied;
_buckets.swap(old);
_buckets.clear();
_buckets.assign(new_cap, Entry{});
_occupied.reset();
_occupied.resize(new_cap);
_deleted.reset();
_deleted.resize(new_cap);
_size = _tombstones = 0;
for (fl::size i = 0; i < old.size(); i++) {
if (old_occupied.test(i))
insert(fl::move(old[i].key), fl::move(old[i].value));
}
}
// Rehash the inline buckets without resizing
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
void rehash_inline_no_resize() {
// filter out tombstones and compact
fl::size cap = _buckets.size();
fl::size pos = 0;
// compact live elements to the left.
for (fl::size i = 0; i < cap; ++i) {
if (is_occupied(i)) {
if (pos != i) {
_buckets[pos] = _buckets[i];
mark_empty(i);
mark_occupied(pos);
}
++pos;
} else if (is_deleted(i)) {
mark_empty(i);
}
}
fl::bitset<1024> occupied; // Preallocate a bitset of size 1024
// swap the components, this will happen at most N times,
// use the occupied bitset to track which entries are occupied
// in the array rather than just copied in.
fl::optional<Entry> tmp;
for (fl::size i = 0; i < _size; ++i) {
const bool already_finished = occupied.test(i);
if (already_finished) {
continue;
}
auto &e = _buckets[i];
// FASTLED_ASSERT(e.state == EntryState::Occupied,
// "HashMap::rehash_inline_no_resize: invalid
// state");
fl::size idx = find_unoccupied_index_using_bitset(e.key, occupied);
if (idx == npos()) {
// no more space
FASTLED_ASSERT(
false, "HashMap::rehash_inline_no_resize: invalid index at "
<< idx << " which is " << npos());
return;
}
// if idx < pos then we are moving the entry to a new location
FASTLED_ASSERT(!tmp,
"HashMap::rehash_inline_no_resize: invalid tmp");
if (idx >= _size) {
// directly copy it now
_buckets[idx] = e;
continue;
}
tmp = e;
occupied.set(idx);
_buckets[idx] = *tmp.ptr();
while (!tmp.empty()) {
// we have to find a place for temp.
// find new position for tmp.
auto key = tmp.ptr()->key;
fl::size new_idx =
find_unoccupied_index_using_bitset(key, occupied);
if (new_idx == npos()) {
// no more space
FASTLED_ASSERT(
false,
"HashMap::rehash_inline_no_resize: invalid index at "
<< new_idx << " which is " << npos());
return;
}
occupied.set(new_idx);
if (new_idx < _size) {
// we have to swap the entry at new_idx with tmp
fl::optional<Entry> tmp2 = _buckets[new_idx];
_buckets[new_idx] = *tmp.ptr();
tmp = tmp2;
} else {
// we can just move tmp to new_idx
_buckets[new_idx] = *tmp.ptr();
tmp.reset();
}
}
FASTLED_ASSERT(
occupied.test(i),
"HashMap::rehash_inline_no_resize: invalid occupied at " << i);
FASTLED_ASSERT(
tmp.empty(), "HashMap::rehash_inline_no_resize: invalid tmp at " << i);
}
// Reset tombstones count since we've cleared all deleted entries
_tombstones = 0;
}
FL_DISABLE_WARNING_POP
fl::vector_inlined<Entry, INLINED_COUNT> _buckets;
fl::size _size;
fl::size _tombstones;
u8 mLoadFactor;
fl::bitset<1024> _occupied;
fl::bitset<1024> _deleted;
Hash _hash;
KeyEqual _equal;
};
// begin using declarations for stl compatibility
template <typename T> using equal_to = EqualTo<T>;
template <typename Key, typename T, typename Hash = Hash<Key>,
typename KeyEqual = equal_to<Key>>
using hash_map = HashMap<Key, T, Hash, KeyEqual>;
template <typename Key, typename T, typename Hash = Hash<Key>,
typename KeyEqual = equal_to<Key>>
using unordered_map = HashMap<Key, T, Hash, KeyEqual>;
// end using declarations for stl compatibility
} // namespace fl
FL_DISABLE_WARNING_POP

View File

@@ -0,0 +1,162 @@
#pragma once
/*
LRU (Least Recently Used) HashMap that is optimized for embedded devices.
This hashmap has a maximum size and will automatically evict the least
recently used items when it reaches capacity.
*/
#include "fl/hash_map.h"
#include "fl/type_traits.h"
namespace fl {
template <typename Key, typename T, typename Hash = Hash<Key>,
typename KeyEqual = EqualTo<Key>,
int INLINED_COUNT = FASTLED_HASHMAP_INLINED_COUNT>
class HashMapLru {
private:
// Wrapper for values that includes access time tracking
struct ValueWithTimestamp {
T value;
u32 last_access_time;
ValueWithTimestamp() : last_access_time(0) {}
ValueWithTimestamp(const T &v, u32 time)
: value(v), last_access_time(time) {}
};
public:
HashMapLru(fl::size max_size) : mMaxSize(max_size), mCurrentTime(0) {
// Ensure max size is at least 1
if (mMaxSize < 1)
mMaxSize = 1;
}
void setMaxSize(fl::size max_size) {
while (mMaxSize < max_size) {
// Evict oldest items until we reach the new max size
evictOldest();
}
mMaxSize = max_size;
}
void swap(HashMapLru &other) {
fl::swap(mMap, other.mMap);
fl::swap(mMaxSize, other.mMaxSize);
fl::swap(mCurrentTime, other.mCurrentTime);
}
// Insert or update a key-value pair
void insert(const Key &key, const T &value) {
// Only evict if we're at capacity AND this is a new key
const ValueWithTimestamp *existing = mMap.find_value(key);
auto curr = mCurrentTime++;
if (existing) {
// Update the value and access time
ValueWithTimestamp &vwt =
const_cast<ValueWithTimestamp &>(*existing);
vwt.value = value;
vwt.last_access_time = curr;
return;
}
if (mMap.size() >= mMaxSize) {
evictOldest();
}
// Insert or update the value with current timestamp
ValueWithTimestamp vwt(value, mCurrentTime);
mMap.insert(key, vwt);
}
// Get value for key, returns nullptr if not found
T *find_value(const Key &key) {
ValueWithTimestamp *vwt = mMap.find_value(key);
if (vwt) {
// Update access time
auto curr = mCurrentTime++;
vwt->last_access_time = curr;
return &vwt->value;
}
return nullptr;
}
// Get value for key, returns nullptr if not found (const version)
const T *find_value(const Key &key) const {
const ValueWithTimestamp *vwt = mMap.find_value(key);
return vwt ? &vwt->value : nullptr;
}
// Access operator - creates entry if not exists
T &operator[](const Key &key) {
// If we're at capacity and this is a new key, evict oldest
auto curr = mCurrentTime++;
auto entry = mMap.find_value(key);
if (entry) {
// Update access time
entry->last_access_time = curr;
return entry->value;
}
if (mMap.size() >= mMaxSize) {
evictOldest();
}
// Get or create entry and update timestamp
// mCurrentTime++;
ValueWithTimestamp &vwt = mMap[key];
vwt.last_access_time = curr;
return vwt.value;
}
// Remove a key
bool remove(const Key &key) { return mMap.remove(key); }
// Clear the map
void clear() {
mMap.clear();
mCurrentTime = 0;
}
// Size accessors
fl::size size() const { return mMap.size(); }
bool empty() const { return mMap.empty(); }
fl::size capacity() const { return mMaxSize; }
private:
// Evict the least recently used item
void evictOldest() {
if (mMap.empty())
return;
// Find the entry with the oldest timestamp
Key oldest_key;
u32 oldest_time = UINT32_MAX;
bool found = false;
for (auto it = mMap.begin(); it != mMap.end(); ++it) {
const auto &entry = *it;
const ValueWithTimestamp &vwt = entry.second;
if (vwt.last_access_time < oldest_time) {
oldest_time = vwt.last_access_time;
oldest_key = entry.first;
found = true;
}
}
// Remove the oldest entry
if (found) {
mMap.remove(oldest_key);
}
}
HashMap<Key, ValueWithTimestamp, Hash, KeyEqual, INLINED_COUNT> mMap;
fl::size mMaxSize;
u32 mCurrentTime;
};
} // namespace fl

View File

@@ -0,0 +1,32 @@
#pragma once
#include "fl/hash_map.h"
namespace fl {
// just define a hashset to be a hashmap with a dummy value
template <typename Key, typename Hash = Hash<Key>,
typename KeyEqual = EqualTo<Key>>
class HashSet : public HashMap<Key, bool, Hash, KeyEqual> {
public:
using Base = HashMap<Key, bool, Hash, KeyEqual>;
using iterator = typename Base::iterator;
using const_iterator = typename Base::const_iterator;
HashSet(fl::size initial_capacity = 8, float max_load = 0.7f)
: Base(initial_capacity, max_load) {}
void insert(const Key &key) { Base::insert(key, true); }
void erase(const Key &key) { Base::erase(key); }
iterator find(const Key &key) { return Base::find(key); }
};
template <typename Key, typename Hash = Hash<Key>,
typename KeyEqual = EqualTo<Key>>
using hash_set = HashSet<Key, Hash, KeyEqual>;
} // namespace fl

View File

@@ -0,0 +1,109 @@
/// @file hsv.h
/// Defines the hue, saturation, and value (HSV) pixel struct
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
namespace fl {
/// @addtogroup PixelTypes Pixel Data Types (CRGB/CHSV)
/// @{
/// Representation of an HSV pixel (hue, saturation, value (aka brightness)).
struct CHSV {
union {
struct {
union {
/// Color hue.
/// This is an 8-bit value representing an angle around
/// the color wheel. Where 0 is 0°, and 255 is 358°.
fl::u8 hue;
fl::u8 h; ///< @copydoc hue
};
union {
/// Color saturation.
/// This is an 8-bit value representing a percentage.
fl::u8 saturation;
fl::u8 sat; ///< @copydoc saturation
fl::u8 s; ///< @copydoc saturation
};
union {
/// Color value (brightness).
/// This is an 8-bit value representing a percentage.
fl::u8 value;
fl::u8 val; ///< @copydoc value
fl::u8 v; ///< @copydoc value
};
};
/// Access the hue, saturation, and value data as an array.
/// Where:
/// * `raw[0]` is the hue
/// * `raw[1]` is the saturation
/// * `raw[2]` is the value
fl::u8 raw[3];
};
/// Array access operator to index into the CHSV object
/// @param x the index to retrieve (0-2)
/// @returns the CHSV::raw value for the given index
inline fl::u8& operator[] (fl::u8 x) __attribute__((always_inline))
{
return raw[x];
}
/// @copydoc operator[]
inline const fl::u8& operator[] (fl::u8 x) const __attribute__((always_inline))
{
return raw[x];
}
/// Default constructor
/// @warning Default values are UNITIALIZED!
constexpr inline CHSV() __attribute__((always_inline)): h(0), s(0), v(0) { }
/// Allow construction from hue, saturation, and value
/// @param ih input hue
/// @param is input saturation
/// @param iv input value
constexpr inline CHSV( fl::u8 ih, fl::u8 is, fl::u8 iv) __attribute__((always_inline))
: h(ih), s(is), v(iv)
{
}
/// Allow copy construction
constexpr inline CHSV(const CHSV& rhs) noexcept : h(rhs.h), s(rhs.s), v(rhs.v) { }
/// Allow copy construction
inline CHSV& operator= (const CHSV& rhs) __attribute__((always_inline)) = default;
/// Assign new HSV values
/// @param ih input hue
/// @param is input saturation
/// @param iv input value
/// @returns reference to the CHSV object
inline CHSV& setHSV(fl::u8 ih, fl::u8 is, fl::u8 iv) __attribute__((always_inline))
{
h = ih;
s = is;
v = iv;
return *this;
}
};
/// Pre-defined hue values for CHSV objects
typedef enum {
HUE_RED = 0, ///< Red (0°)
HUE_ORANGE = 32, ///< Orange (45°)
HUE_YELLOW = 64, ///< Yellow (90°)
HUE_GREEN = 96, ///< Green (135°)
HUE_AQUA = 128, ///< Aqua (180°)
HUE_BLUE = 160, ///< Blue (225°)
HUE_PURPLE = 192, ///< Purple (270°)
HUE_PINK = 224 ///< Pink (315°)
} HSVHue;
/// @} PixelTypes
} // namespace fl

View File

@@ -0,0 +1,197 @@
#include "fl/hsv16.h"
#include "fl/math.h"
#include "lib8tion/intmap.h"
#include "fl/ease.h"
namespace fl {
// Improved 8-bit to 16-bit scaling using the same technique as map8_to_16
// but with proper rounding for the 0-255 to 0-65535 conversion
static inline u16 scale8_to_16_accurate(u8 x) {
if (x == 0) return 0;
if (x == 255) return 65535;
// Use 32-bit arithmetic with rounding: (x * 65535 + 127) / 255
// This is equivalent to: (x * 65535 + 255/2) / 255
return (u16)(((u32)x * 65535 + 127) / 255);
}
static HSV16 RGBtoHSV16(const CRGB &rgb) {
// Work with 8-bit values directly
u8 r = rgb.r;
u8 g = rgb.g;
u8 b = rgb.b;
// Find min and max
u8 mx = fl_max(r, fl_max(g, b));
u8 mn = fl_min(r, fl_min(g, b));
u8 delta = mx - mn;
u16 h = 0;
u16 s = 0;
u16 v = scale8_to_16_accurate(mx);
// Calculate saturation using improved scaling
if (mx > 0) {
// s = (delta * 65535) / mx, but with better accuracy
// Use the same technique as scale8_to_16_accurate but for arbitrary denominator
if (delta == mx) {
s = 65535; // Saturation is 100%
} else {
s = (u16)(((u32)delta * 65535 + (mx >> 1)) / mx);
}
}
// Calculate hue using improved algorithms
if (delta > 0) {
u32 hue_calc = 0;
if (mx == r) {
// Hue in red sector (0-60 degrees)
if (g >= b) {
// Use improved division: hue_calc = (g - b) * 65535 / (6 * delta)
u32 numerator = (u32)(g - b) * 65535;
if (delta <= 42) { // 6 * 42 = 252, safe for small delta
hue_calc = numerator / (6 * delta);
} else {
hue_calc = numerator / delta / 6; // Avoid overflow
}
} else {
u32 numerator = (u32)(b - g) * 65535;
if (delta <= 42) {
hue_calc = 65535 - numerator / (6 * delta);
} else {
hue_calc = 65535 - numerator / delta / 6;
}
}
} else if (mx == g) {
// Hue in green sector (60-180 degrees)
// Handle signed arithmetic properly to avoid integer underflow
i32 signed_diff = (i32)b - (i32)r;
u32 sector_offset = 65535 / 3; // 60 degrees (120 degrees in 16-bit space)
if (signed_diff >= 0) {
// Positive case: b >= r
u32 numerator = (u32)signed_diff * 65535;
if (delta <= 42) {
hue_calc = sector_offset + numerator / (6 * delta);
} else {
hue_calc = sector_offset + numerator / delta / 6;
}
} else {
// Negative case: b < r
u32 numerator = (u32)(-signed_diff) * 65535;
if (delta <= 42) {
hue_calc = sector_offset - numerator / (6 * delta);
} else {
hue_calc = sector_offset - numerator / delta / 6;
}
}
} else { // mx == b
// Hue in blue sector (180-300 degrees)
// Handle signed arithmetic properly to avoid integer underflow
i32 signed_diff = (i32)r - (i32)g;
u32 sector_offset = (2 * 65535) / 3; // 240 degrees (240 degrees in 16-bit space)
if (signed_diff >= 0) {
// Positive case: r >= g
u32 numerator = (u32)signed_diff * 65535;
if (delta <= 42) {
hue_calc = sector_offset + numerator / (6 * delta);
} else {
hue_calc = sector_offset + numerator / delta / 6;
}
} else {
// Negative case: r < g
u32 numerator = (u32)(-signed_diff) * 65535;
if (delta <= 42) {
hue_calc = sector_offset - numerator / (6 * delta);
} else {
hue_calc = sector_offset - numerator / delta / 6;
}
}
}
h = (u16)(hue_calc & 0xFFFF);
}
return HSV16{h, s, v};
}
static CRGB HSV16toRGB(const HSV16& hsv) {
// Convert 16-bit values to working range
u32 h = hsv.h;
u32 s = hsv.s;
u32 v = hsv.v;
if (s == 0) {
// Grayscale case - use precise mapping
u8 gray = map16_to_8(v);
return CRGB{gray, gray, gray};
}
// Determine which sector of the color wheel (0-5)
u32 sector = (h * 6) / 65536;
u32 sector_pos = (h * 6) % 65536; // Position within sector (0-65535)
// Calculate intermediate values using precise mapping
// c = v * s / 65536, with proper rounding
u32 c = map32_to_16(v * s);
// Calculate x = c * (1 - |2*(sector_pos/65536) - 1|)
u32 x;
if (sector & 1) {
// For odd sectors (1, 3, 5), we want decreasing values
// x = c * (65535 - sector_pos) / 65535
x = map32_to_16(c * (65535 - sector_pos));
} else {
// For even sectors (0, 2, 4), we want increasing values
// x = c * sector_pos / 65535
x = map32_to_16(c * sector_pos);
}
u32 m = v - c;
u32 r1, g1, b1;
switch (sector) {
case 0: r1 = c; g1 = x; b1 = 0; break;
case 1: r1 = x; g1 = c; b1 = 0; break;
case 2: r1 = 0; g1 = c; b1 = x; break;
case 3: r1 = 0; g1 = x; b1 = c; break;
case 4: r1 = x; g1 = 0; b1 = c; break;
default: r1 = c; g1 = 0; b1 = x; break;
}
// Add baseline and scale to 8-bit using accurate mapping
u8 R = map16_to_8(u16(r1 + m));
u8 G = map16_to_8(u16(g1 + m));
u8 B = map16_to_8(u16(b1 + m));
return CRGB{R, G, B};
}
HSV16::HSV16(const CRGB& rgb) {
*this = RGBtoHSV16(rgb);
}
CRGB HSV16::ToRGB() const {
return HSV16toRGB(*this);
}
CRGB HSV16::colorBoost(EaseType saturation_function, EaseType luminance_function) const {
HSV16 hsv = *this;
if (saturation_function != EASE_NONE) {
u16 inv_sat = 65535 - hsv.s;
inv_sat = ease16(saturation_function, inv_sat);
hsv.s = (65535 - inv_sat);
}
if (luminance_function != EASE_NONE) {
hsv.v = ease16(luminance_function, hsv.v);
}
return hsv.ToRGB();
}
} // namespace fl

View File

@@ -0,0 +1,38 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
#include "crgb.h"
#include "fl/ease.h"
namespace fl {
struct HSV16 {
u16 h = 0;
u16 s = 0;
u16 v = 0;
HSV16() = default;
HSV16(u16 h, u16 s, u16 v) : h(h), s(s), v(v) {}
HSV16(const CRGB& rgb);
// Rule of 5 for POD data
HSV16(const HSV16 &other) = default;
HSV16 &operator=(const HSV16 &other) = default;
HSV16(HSV16 &&other) noexcept = default;
HSV16 &operator=(HSV16 &&other) noexcept = default;
CRGB ToRGB() const;
/// Automatic conversion operator to CRGB
/// Allows HSV16 to be automatically converted to CRGB
operator CRGB() const { return ToRGB(); }
// Are you using WS2812 (or other RGB8 LEDS) to display video?
// decimate the color? Use colorBoost() to boost the saturation.
// This works great for WS2812 and any other RGB8 LEDs.
// Default saturation function is similar to gamma correction.
CRGB colorBoost(EaseType saturation_function = EASE_IN_QUAD, EaseType luminance_function = EASE_NONE) const;
};
} // namespace fl

View File

@@ -0,0 +1,81 @@
#include "fl/id_tracker.h"
namespace fl {
int IdTracker::getOrCreateId(void* ptr) {
if (!ptr) {
return -1; // Invalid pointer gets invalid ID
}
// Lock for thread safety
mMutex.lock();
// Check if ID already exists
const int* existingId = mPointerToId.find_value(ptr);
if (existingId) {
int id = *existingId;
mMutex.unlock();
return id;
}
// Create new ID
int newId = mNextId++;
mPointerToId[ptr] = newId;
mMutex.unlock();
return newId;
}
bool IdTracker::getId(void* ptr, int* outId) {
if (!ptr || !outId) {
return false;
}
// Lock for thread safety
mMutex.lock();
const int* existingId = mPointerToId.find_value(ptr);
bool found = (existingId != nullptr);
if (found) {
*outId = *existingId;
}
mMutex.unlock();
return found;
}
bool IdTracker::removeId(void* ptr) {
if (!ptr) {
return false;
}
// Lock for thread safety
mMutex.lock();
bool removed = mPointerToId.erase(ptr);
mMutex.unlock();
return removed;
}
size_t IdTracker::size() {
// Lock for thread safety
mMutex.lock();
size_t currentSize = mPointerToId.size();
mMutex.unlock();
return currentSize;
}
void IdTracker::clear() {
// Lock for thread safety
mMutex.lock();
mPointerToId.clear();
mNextId = 0; // Reset ID counter to start at 0
mMutex.unlock();
}
} // namespace fl

View File

@@ -0,0 +1,96 @@
#pragma once
#include "fl/hash_map.h"
#include "fl/mutex.h"
#include "fl/namespace.h"
namespace fl {
/**
* Thread-safe ID tracker that maps void* pointers to unique integer IDs.
*
* Features:
* - Auto-incrementing ID counter for new entries
* - Thread-safe operations with mutex protection
* - Instantiable class - create as many trackers as needed
* - Support for removal of tracked pointers
*
* Usage:
* IdTracker tracker; // Create instance
* int id = tracker.getOrCreateId(ptr);
* bool found = tracker.getId(ptr, &id);
* tracker.removeId(ptr);
*
* For singleton behavior, wrap in your own singleton:
* static IdTracker& getGlobalTracker() {
* static IdTracker instance;
* return instance;
* }
*/
class IdTracker {
public:
/**
* Default constructor - creates a new ID tracker instance
*/
IdTracker() = default;
/**
* Get existing ID for pointer, or create a new one if not found.
* Thread-safe.
*
* @param ptr Pointer to track
* @return Unique integer ID for this pointer
*/
int getOrCreateId(void* ptr);
/**
* Get existing ID for pointer without creating a new one.
* Thread-safe.
*
* @param ptr Pointer to look up
* @param outId Pointer to store the ID if found
* @return true if ID was found, false if pointer not tracked
*/
bool getId(void* ptr, int* outId);
/**
* Remove tracking for a pointer.
* Thread-safe.
*
* @param ptr Pointer to stop tracking
* @return true if pointer was being tracked and removed, false if not found
*/
bool removeId(void* ptr);
/**
* Get the current number of tracked pointers.
* Thread-safe.
*
* @return Number of currently tracked pointers
*/
size_t size();
/**
* Clear all tracked pointers and reset ID counter.
* Thread-safe.
*/
void clear();
// Non-copyable and non-movable for thread safety
// (Each instance should have its own independent state)
IdTracker(const IdTracker&) = delete;
IdTracker& operator=(const IdTracker&) = delete;
IdTracker(IdTracker&&) = delete;
IdTracker& operator=(IdTracker&&) = delete;
private:
// Thread synchronization
mutable fl::mutex mMutex;
// ID mapping and counter
fl::hash_map<void*, int> mPointerToId;
int mNextId = 0; // Start IDs at 0 to match StripIdMap behavior
};
} // namespace fl

View File

@@ -0,0 +1,62 @@
// allow-include-after-namespace
#pragma once
#include "fl/namespace.h"
// Define if initializer_list is available
// Check for C++11 and if std::initializer_list exists
#if defined(__AVR__)
// Emulated initializer_list for AVR platforms
namespace fl {
template<typename T>
class initializer_list {
private:
const T* mBegin;
fl::size mSize;
// Private constructor used by compiler
constexpr initializer_list(const T* first, fl::size size)
: mBegin(first), mSize(size) {}
public:
using value_type = T;
using reference = const T&;
using const_reference = const T&;
using size_type = fl::size;
using iterator = const T*;
using const_iterator = const T*;
// Default constructor
constexpr initializer_list() : mBegin(nullptr), mSize(0) {}
// Size and capacity
constexpr fl::size size() const { return mSize; }
constexpr bool empty() const { return mSize == 0; }
// Iterators
constexpr const_iterator begin() const { return mBegin; }
constexpr const_iterator end() const { return mBegin + mSize; }
// Allow compiler access to private constructor
template<typename U> friend class initializer_list;
};
// Helper functions to match std::initializer_list interface
template<typename T>
constexpr const T* begin(initializer_list<T> il) {
return il.begin();
}
template<typename T>
constexpr const T* end(initializer_list<T> il) {
return il.end();
}
}
#else
#include <initializer_list>
namespace fl {
using std::initializer_list;
}
#endif

View File

@@ -0,0 +1,22 @@
#pragma once
#include "fl/stdint.h"
#include "fl/int.h"
// This file must not be in the fl namespace, it must be in the global
// namespace.
#if (defined(__AVR__) || !defined(__has_include)) && (!defined(FASTLED_HAS_NEW))
#ifndef __has_include
#define _NO_EXCEPT
#else
#define _NO_EXCEPT noexcept
#endif
inline void *operator new(fl::size, void *ptr) _NO_EXCEPT { return ptr; }
#elif __has_include(<new>)
#include <new>
#elif __has_include(<new.h>)
#include <new.h>
#elif __has_include("new.h")
#include "new.h"
#endif

View File

@@ -0,0 +1,16 @@
#pragma once
namespace fl {
// Because of the fixed size nature of a lot of FastLED's containers we
// need to provide additional feedback to the caller about the nature of
// why an insert did or did not happen. Specifically, we want to differentiate
// between failing to insert because the item already existed and when the
// container was full.
enum InsertResult {
kInserted = 0,
kExists = 1,
kMaxSize = 2,
};
} // namespace fl

View File

@@ -0,0 +1,94 @@
#pragma once
/// IMPORTANT!
/// This file MUST not
#include "fl/stdint.h" // For uintptr_t and size_t
// Platform-specific integer type definitions
// This includes platform-specific 16/32/64-bit types
#include "platforms/int.h"
namespace fl {
// 8-bit types - char is reliably 8 bits on all supported platforms
// These must be defined BEFORE platform includes so fractional types can use them
typedef signed char i8;
typedef unsigned char u8;
typedef unsigned int uint;
// Pointer and size types are defined per-platform in platforms/int.h
// uptr (pointer type) and size (size type) are defined per-platform
}
namespace fl {
///////////////////////////////////////////////////////////////////////
///
/// Fixed-Point Fractional Types.
/// Types for storing fractional data.
///
/// * ::sfract7 should be interpreted as signed 128ths.
/// * ::fract8 should be interpreted as unsigned 256ths.
/// * ::sfract15 should be interpreted as signed 32768ths.
/// * ::fract16 should be interpreted as unsigned 65536ths.
///
/// Example: if a fract8 has the value "64", that should be interpreted
/// as 64/256ths, or one-quarter.
///
/// accumXY types should be interpreted as X bits of integer,
/// and Y bits of fraction.
/// E.g., ::accum88 has 8 bits of int, 8 bits of fraction
///
/// ANSI: unsigned short _Fract.
/// Range is 0 to 0.99609375 in steps of 0.00390625.
/// Should be interpreted as unsigned 256ths.
typedef u8 fract8;
/// ANSI: signed short _Fract.
/// Range is -0.9921875 to 0.9921875 in steps of 0.0078125.
/// Should be interpreted as signed 128ths.
typedef i8 sfract7;
/// ANSI: unsigned _Fract.
/// Range is 0 to 0.99998474121 in steps of 0.00001525878.
/// Should be interpreted as unsigned 65536ths.
typedef u16 fract16;
typedef i32 sfract31; ///< ANSI: signed long _Fract. 31 bits int, 1 bit fraction
typedef u32 fract32; ///< ANSI: unsigned long _Fract. 32 bits int, 32 bits fraction
/// ANSI: signed _Fract.
/// Range is -0.99996948242 to 0.99996948242 in steps of 0.00003051757.
/// Should be interpreted as signed 32768ths.
typedef i16 sfract15;
typedef u16 accum88; ///< ANSI: unsigned short _Accum. 8 bits int, 8 bits fraction
typedef i16 saccum78; ///< ANSI: signed short _Accum. 7 bits int, 8 bits fraction
typedef u32 accum1616; ///< ANSI: signed _Accum. 16 bits int, 16 bits fraction
typedef i32 saccum1516; ///< ANSI: signed _Accum. 15 bits int, 16 bits fraction
typedef u16 accum124; ///< no direct ANSI counterpart. 12 bits int, 4 bits fraction
typedef i32 saccum114; ///< no direct ANSI counterpart. 1 bit int, 14 bits fraction
}
namespace fl {
// Size assertions moved to src/platforms/compile_test.cpp.hpp
}
// Make fractional types available in global namespace
using fl::fract8;
using fl::sfract7;
using fl::fract16;
using fl::sfract31;
using fl::fract32;
using fl::sfract15;
using fl::accum88;
using fl::saccum78;
using fl::accum1616;
using fl::saccum1516;
using fl::accum124;
using fl::saccum114;

View File

@@ -0,0 +1,225 @@
#include "io.h"
#include "fl/stdint.h"
// Platform-specific I/O implementations
#ifdef __EMSCRIPTEN__
#include "platforms/wasm/io_wasm.h"
#elif defined(FASTLED_TESTING) || defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
#include "platforms/io_native.h"
#elif defined(ESP32) || defined(ESP8266)
#include "platforms/esp/io_esp.h"
#elif defined(__AVR__) && !defined(ARDUINO_ARCH_MEGAAVR)
#include "platforms/avr/io_avr.h"
#elif defined(__MKL26Z64__)
// Teensy LC has special handling to avoid _write linker issues
#include "platforms/io_teensy_lc.h"
#elif defined(__IMXRT1062__) || defined(__MK66FX1M0__) || defined(__MK64FX512__) || defined(__MK20DX256__) || defined(__MK20DX128__)
// All other Teensy platforms use lightweight implementation to avoid Serial library bloat
#include "platforms/io_teensy.h"
#else
#include "platforms/io_arduino.h"
#endif
namespace fl {
#ifdef FASTLED_TESTING
// Static storage for injected handlers using lazy initialization to avoid global constructors
static print_handler_t& get_print_handler() {
static print_handler_t handler;
return handler;
}
static println_handler_t& get_println_handler() {
static println_handler_t handler;
return handler;
}
static available_handler_t& get_available_handler() {
static available_handler_t handler;
return handler;
}
static read_handler_t& get_read_handler() {
static read_handler_t handler;
return handler;
}
#endif
void print(const char* str) {
if (!str) return;
#ifdef FASTLED_TESTING
// Check for injected handler first
if (get_print_handler()) {
get_print_handler()(str);
return;
}
#endif
#ifdef __EMSCRIPTEN__
print_wasm(str);
#elif defined(FASTLED_TESTING) || defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
print_native(str);
#elif defined(ESP32) || defined(ESP8266)
print_esp(str);
#elif defined(__AVR__) && !defined(ARDUINO_ARCH_MEGAAVR)
print_avr(str);
#elif defined(__MKL26Z64__)
// Teensy LC uses special no-op functions to avoid _write linker issues
print_teensy_lc(str);
#elif defined(__IMXRT1062__) || defined(__MK66FX1M0__) || defined(__MK64FX512__) || defined(__MK20DX256__) || defined(__MK20DX128__)
// All other Teensy platforms use lightweight implementation
print_teensy(str);
#else
// Use generic Arduino print for all other platforms including:
// - STM32 (STM32F1, STM32F4, STM32H7, ARDUINO_GIGA)
// - NRF (NRF52, NRF52832, NRF52840, ARDUINO_NRF52_DK)
// - All other Arduino-compatible platforms
print_arduino(str);
#endif
}
void println(const char* str) {
if (!str) return;
#ifdef FASTLED_TESTING
// Check for injected handler first
if (get_println_handler()) {
get_println_handler()(str);
return;
}
#endif
#ifdef __EMSCRIPTEN__
println_wasm(str);
#elif defined(FASTLED_TESTING) || defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
println_native(str);
#elif defined(ESP32) || defined(ESP8266)
println_esp(str);
#elif defined(__AVR__) && !defined(ARDUINO_ARCH_MEGAAVR)
println_avr(str);
#elif defined(__MKL26Z64__)
// Teensy LC uses special no-op functions to avoid _write linker issues
println_teensy_lc(str);
#elif defined(__IMXRT1062__) || defined(__MK66FX1M0__) || defined(__MK64FX512__) || defined(__MK20DX256__) || defined(__MK20DX128__)
// All other Teensy platforms use lightweight implementation
println_teensy(str);
#else
// Use generic Arduino print for all other platforms including:
// - STM32 (STM32F1, STM32F4, STM32H7, ARDUINO_GIGA)
// - NRF (NRF52, NRF52832, NRF52840, ARDUINO_NRF52_DK)
// - All other Arduino-compatible platforms
println_arduino(str);
#endif
}
int available() {
#ifdef FASTLED_TESTING
// Check for injected handler first
if (get_available_handler()) {
return get_available_handler()();
}
#endif
#ifdef __EMSCRIPTEN__
return available_wasm();
#elif defined(FASTLED_TESTING) || defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
return available_native();
#elif defined(ESP32) || defined(ESP8266)
return available_esp();
#elif defined(__AVR__) && !defined(ARDUINO_ARCH_MEGAAVR)
return available_avr();
#elif defined(__MKL26Z64__)
// Teensy LC uses special no-op functions to avoid _write linker issues
return available_teensy_lc();
#elif defined(__IMXRT1062__) || defined(__MK66FX1M0__) || defined(__MK64FX512__) || defined(__MK20DX256__) || defined(__MK20DX128__)
// All other Teensy platforms use lightweight implementation
return available_teensy();
#else
// Use generic Arduino input for all other platforms including:
// - STM32 (STM32F1, STM32F4, STM32H7, ARDUINO_GIGA)
// - NRF (NRF52, NRF52832, NRF52840, ARDUINO_NRF52_DK)
// - All other Arduino-compatible platforms
return available_arduino();
#endif
}
int read() {
#ifdef FASTLED_TESTING
// Check for injected handler first
if (get_read_handler()) {
return get_read_handler()();
}
#endif
#ifdef __EMSCRIPTEN__
return read_wasm();
#elif defined(FASTLED_TESTING) || defined(__linux__) || defined(__APPLE__) || defined(_WIN32)
return read_native();
#elif defined(ESP32) || defined(ESP8266)
return read_esp();
#elif defined(__AVR__) && !defined(ARDUINO_ARCH_MEGAAVR)
return read_avr();
#elif defined(__MKL26Z64__)
// Teensy LC uses special no-op functions to avoid _write linker issues
return read_teensy_lc();
#elif defined(__IMXRT1062__) || defined(__MK66FX1M0__) || defined(__MK64FX512__) || defined(__MK20DX256__) || defined(__MK20DX128__)
// All other Teensy platforms use lightweight implementation
return read_teensy();
#else
// Use generic Arduino input for all other platforms including:
// - STM32 (STM32F1, STM32F4, STM32H7, ARDUINO_GIGA)
// - NRF (NRF52, NRF52832, NRF52840, ARDUINO_NRF52_DK)
// - All other Arduino-compatible platforms
return read_arduino();
#endif
}
#ifdef FASTLED_TESTING
// Inject function handlers for testing
void inject_print_handler(const print_handler_t& handler) {
get_print_handler() = handler;
}
void inject_println_handler(const println_handler_t& handler) {
get_println_handler() = handler;
}
void inject_available_handler(const available_handler_t& handler) {
get_available_handler() = handler;
}
void inject_read_handler(const read_handler_t& handler) {
get_read_handler() = handler;
}
// Clear all injected handlers (restores default behavior)
void clear_io_handlers() {
get_print_handler() = print_handler_t{};
get_println_handler() = println_handler_t{};
get_available_handler() = available_handler_t{};
get_read_handler() = read_handler_t{};
}
// Clear individual handlers
void clear_print_handler() {
get_print_handler() = print_handler_t{};
}
void clear_println_handler() {
get_println_handler() = println_handler_t{};
}
void clear_available_handler() {
get_available_handler() = available_handler_t{};
}
void clear_read_handler() {
get_read_handler() = read_handler_t{};
}
#endif // FASTLED_TESTING
} // namespace fl

View File

@@ -0,0 +1,61 @@
#pragma once
#define FL_IO_H_INCLUDED
#ifdef FASTLED_TESTING
#include "fl/function.h"
#endif
namespace fl {
// Low-level print functions that avoid printf/sprintf dependencies
// These use the most efficient output method for each platform
// Print a string without newline
void print(const char* str);
// Print a string with newline
#ifndef FL_DBG_PRINTLN_DECLARED
void println(const char* str);
#else
// Declaration already exists from fl/dbg.h
#endif
// Low-level input functions that provide Serial-style read functionality
// These use the most efficient input method for each platform
// Returns the number of bytes available to read from input stream
// Returns 0 if no data is available
int available();
// Reads the next byte from input stream
// Returns the byte value (0-255) if data is available
// Returns -1 if no data is available
int read();
#ifdef FASTLED_TESTING
// Testing function handler types
using print_handler_t = fl::function<void(const char*)>;
using println_handler_t = fl::function<void(const char*)>;
using available_handler_t = fl::function<int()>;
using read_handler_t = fl::function<int()>;
// Inject function handlers for testing
void inject_print_handler(const print_handler_t& handler);
void inject_println_handler(const println_handler_t& handler);
void inject_available_handler(const available_handler_t& handler);
void inject_read_handler(const read_handler_t& handler);
// Clear all injected handlers (restores default behavior)
void clear_io_handlers();
// Clear individual handlers
void clear_print_handler();
void clear_println_handler();
void clear_available_handler();
void clear_read_handler();
#endif // FASTLED_TESTING
} // namespace fl

View File

@@ -0,0 +1,4 @@
#pragma once
#include "fl/ostream.h"
#include "fl/istream.h"

View File

@@ -0,0 +1,416 @@
#include "istream.h"
#include "fl/math.h"
#include "fl/compiler_control.h"
//#include <stddef.h>
// <cstdlib> not available on AVR platforms like Arduino UNO
// We implement custom integer parsing functions instead
#include "fl/math_macros.h"
namespace fl {
namespace {
// Helper function to check if a character is a digit
inline bool isDigit(char c) {
return c >= '0' && c <= '9';
}
// Helper function to check if a character is whitespace
inline bool isSpace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
}
// Custom integer parsing function for signed 32-bit integers
bool parse_i32(const char* str, fl::i32& result) {
if (!str) return false;
// Skip leading whitespace
while (*str && isSpace(*str)) {
str++;
}
if (*str == '\0') return false;
bool negative = false;
if (*str == '-') {
negative = true;
str++;
} else if (*str == '+') {
str++;
}
if (*str == '\0' || !isDigit(*str)) return false;
fl::u32 value = 0;
const fl::u32 max_div_10 = 214748364U; // INT32_MAX / 10
const fl::u32 max_mod_10 = 7U; // INT32_MAX % 10
const fl::u32 max_neg_div_10 = 214748364U; // -INT32_MIN / 10
const fl::u32 max_neg_mod_10 = 8U; // -INT32_MIN % 10
while (*str && isDigit(*str)) {
fl::u32 digit = *str - '0';
// Check for overflow
if (negative) {
if (value > max_neg_div_10 || (value == max_neg_div_10 && digit > max_neg_mod_10)) {
return false; // Overflow
}
} else {
if (value > max_div_10 || (value == max_div_10 && digit > max_mod_10)) {
return false; // Overflow
}
}
value = value * 10 + digit;
str++;
}
// Check if we stopped at a non-digit character (should be end of string for valid parse)
if (*str != '\0') return false;
if (negative) {
result = -static_cast<fl::i32>(value);
} else {
result = static_cast<fl::i32>(value);
}
return true;
}
// Custom integer parsing function for unsigned 32-bit integers
bool parse_u32(const char* str, fl::u32& result) {
if (!str) return false;
// Skip leading whitespace
while (*str && isSpace(*str)) {
str++;
}
if (*str == '\0') return false;
// Optional '+' sign (no '-' for unsigned)
if (*str == '+') {
str++;
} else if (*str == '-') {
return false; // Negative not allowed for unsigned
}
if (*str == '\0' || !isDigit(*str)) return false;
fl::u32 value = 0;
const fl::u32 max_div_10 = 429496729U; // UINT32_MAX / 10
const fl::u32 max_mod_10 = 5U; // UINT32_MAX % 10
while (*str && isDigit(*str)) {
fl::u32 digit = *str - '0';
// Check for overflow
if (value > max_div_10 || (value == max_div_10 && digit > max_mod_10)) {
return false; // Overflow
}
value = value * 10 + digit;
str++;
}
// Check if we stopped at a non-digit character (should be end of string for valid parse)
if (*str != '\0') return false;
result = value;
return true;
}
}
FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS
// Global cin instance (stub that conditionally delegates)
istream cin;
// Function to get singleton instance of istream_real (for better linker elimination)
istream_real& cin_real() {
// Local static instance - only constructed when first called
// This allows the linker to eliminate it if never referenced
static istream_real instance;
return instance;
}
bool istream_real::readLine() {
// If we have no more data available and no buffered data, we're at EOF
if (pos_ >= buffer_len_ && fl::available() == 0) {
return false;
}
// Read characters until newline or no more data
buffer_len_ = 0;
while (fl::available() > 0 && buffer_len_ < BUFFER_SIZE - 1) {
int c = fl::read();
if (c == -1) break;
if (c == '\n') break;
if (c == '\r') continue; // Skip carriage return
buffer_[buffer_len_++] = static_cast<char>(c);
}
// Null terminate the buffer
buffer_[buffer_len_] = '\0';
pos_ = 0;
return true;
}
void istream_real::skipWhitespace() {
while (pos_ < buffer_len_ &&
(buffer_[pos_] == ' ' || buffer_[pos_] == '\t' ||
buffer_[pos_] == '\n' || buffer_[pos_] == '\r')) {
pos_++;
}
// If we've consumed all buffer and there's more input, read more
if (pos_ >= buffer_len_ && fl::available() > 0) {
if (readLine()) {
skipWhitespace();
}
}
}
bool istream_real::readToken(string& token) {
skipWhitespace();
if (pos_ >= buffer_len_ && fl::available() == 0) {
failed_ = true;
return false;
}
// If buffer is empty but data is available, read a line
if (pos_ >= buffer_len_ && fl::available() > 0) {
if (!readLine()) {
failed_ = true;
return false;
}
skipWhitespace();
}
// Read until whitespace or end of buffer
token.clear();
while (pos_ < buffer_len_ &&
buffer_[pos_] != ' ' && buffer_[pos_] != '\t' &&
buffer_[pos_] != '\n' && buffer_[pos_] != '\r') {
// Explicitly append as a character string to avoid fl::u8->number conversion
char ch[2] = {buffer_[pos_], '\0'};
token.append(ch, 1);
pos_++;
}
return !token.empty();
}
istream_real& istream_real::operator>>(string& str) {
if (!readToken(str)) {
failed_ = true;
}
return *this;
}
istream_real& istream_real::operator>>(char& c) {
skipWhitespace();
if (pos_ >= buffer_len_ && fl::available() > 0) {
if (!readLine()) {
failed_ = true;
return *this;
}
skipWhitespace();
}
if (pos_ < buffer_len_) {
c = buffer_[pos_];
pos_++;
} else {
failed_ = true;
}
return *this;
}
istream_real& istream_real::operator>>(fl::i8& n) {
string token;
if (readToken(token)) {
fl::i32 temp;
if (parse_i32(token.c_str(), temp) && temp >= -128 && temp <= 127) {
n = static_cast<fl::i8>(temp);
} else {
failed_ = true;
}
} else {
failed_ = true;
}
return *this;
}
istream_real& istream_real::operator>>(fl::u8& n) {
string token;
if (readToken(token)) {
fl::u32 temp;
if (parse_u32(token.c_str(), temp) && temp <= 255) {
n = static_cast<fl::u8>(temp);
} else {
failed_ = true;
}
} else {
failed_ = true;
}
return *this;
}
istream_real& istream_real::operator>>(fl::i16& n) {
string token;
if (readToken(token)) {
fl::i32 temp;
if (parse_i32(token.c_str(), temp) && temp >= -32768 && temp <= 32767) {
n = static_cast<fl::i16>(temp);
} else {
failed_ = true;
}
} else {
failed_ = true;
}
return *this;
}
// u16 operator>> removed - now handled by template in header
istream_real& istream_real::operator>>(fl::i32& n) {
string token;
if (readToken(token)) {
if (!parse_i32(token.c_str(), n)) {
failed_ = true;
}
} else {
failed_ = true;
}
return *this;
}
istream_real& istream_real::operator>>(fl::u32& n) {
string token;
if (readToken(token)) {
if (!parse_u32(token.c_str(), n)) {
failed_ = true;
}
} else {
failed_ = true;
}
return *this;
}
istream_real& istream_real::operator>>(float& f) {
string token;
if (readToken(token)) {
// Use the existing toFloat() method
f = token.toFloat();
// Check if parsing was successful by checking for valid float
// toFloat() returns 0.0f for invalid input, but we need to distinguish
// between actual 0.0f and parse failure
if (ALMOST_EQUAL_FLOAT(f, 0.0f) && token != "0" && token != "0.0" && token != "0." && token.find("0") != 0) {
failed_ = true;
}
} else {
failed_ = true;
}
return *this;
}
istream_real& istream_real::operator>>(double& d) {
string token;
if (readToken(token)) {
// Use the existing toFloat() method
float f = token.toFloat();
d = static_cast<double>(f);
// Check if parsing was successful (same logic as float)
if (ALMOST_EQUAL_FLOAT(f, 0.0f) && token != "0" && token != "0.0" && token != "0." && token.find("0") != 0) {
failed_ = true;
}
} else {
failed_ = true;
}
return *this;
}
// fl::size operator>> removed - now handled by template in header
istream_real& istream_real::getline(string& str) {
str.clear();
// Read from current buffer position to end
while (pos_ < buffer_len_) {
if (buffer_[pos_] == '\n') {
pos_++; // Consume the newline
return *this;
}
// Explicitly append as a character string to avoid fl::u8->number conversion
char ch[2] = {buffer_[pos_], '\0'};
str.append(ch, 1);
pos_++;
}
// If we need more data, read a new line
if (fl::available() > 0) {
// Read more characters until newline
while (fl::available() > 0) {
int c = fl::read();
if (c == -1) break;
if (c == '\n') break;
if (c == '\r') continue; // Skip carriage return
// Explicitly append as a character string to avoid fl::u8->number conversion
char ch[2] = {static_cast<char>(c), '\0'};
str.append(ch, 1);
}
}
return *this;
}
int istream_real::get() {
if (pos_ >= buffer_len_ && fl::available() > 0) {
if (!readLine()) {
return -1;
}
}
if (pos_ < buffer_len_) {
return static_cast<int>(static_cast<unsigned char>(buffer_[pos_++]));
}
// Try to read directly from input if buffer is empty
return fl::read();
}
istream_real& istream_real::putback(char c) {
if (pos_ > 0) {
pos_--;
buffer_[pos_] = c;
} else {
// Insert at beginning of buffer - shift existing data
if (buffer_len_ < BUFFER_SIZE - 1) {
for (fl::size i = buffer_len_; i > 0; --i) {
buffer_[i] = buffer_[i-1];
}
buffer_[0] = c;
buffer_len_++;
buffer_[buffer_len_] = '\0';
}
}
return *this;
}
int istream_real::peek() {
if (pos_ >= buffer_len_ && fl::available() > 0) {
if (!readLine()) {
return -1;
}
}
if (pos_ < buffer_len_) {
return static_cast<int>(static_cast<unsigned char>(buffer_[pos_]));
}
return -1;
}
} // namespace fl

View File

@@ -0,0 +1,298 @@
// allow-include-after-namespace
#pragma once
// Forward declarations to avoid pulling in fl/io.h and causing fl/io.cpp to be compiled
#ifndef FL_IO_H_INCLUDED
namespace fl {
int available();
int read();
#ifdef FASTLED_TESTING
template<typename T> class function; // Forward declare
void clear_io_handlers();
void inject_available_handler(const function<int()>& handler);
void inject_read_handler(const function<int()>& handler);
#endif
}
#endif
#include "fl/str.h"
#include "fl/type_traits.h"
#include "fl/sketch_macros.h"
#include "fl/int.h"
namespace fl {
class istream_real {
private:
static const fl::size BUFFER_SIZE = 256;
char buffer_[BUFFER_SIZE];
fl::size buffer_len_ = 0;
fl::size pos_ = 0;
bool failed_ = false;
// Helper to read a line from input
bool readLine();
// Helper to skip whitespace
void skipWhitespace();
// Helper to read until whitespace or end
bool readToken(string& token);
public:
istream_real() = default;
// Check if stream is in good state
bool good() const { return !failed_; }
bool fail() const { return failed_; }
bool eof() const { return pos_ >= buffer_len_ && fl::available() == 0; }
// Clear error state
void clear() { failed_ = false; }
// Stream input operators
istream_real& operator>>(string& str);
istream_real& operator>>(char& c);
istream_real& operator>>(fl::i8& n);
istream_real& operator>>(fl::u8& n);
istream_real& operator>>(fl::i16& n);
istream_real& operator>>(fl::i32& n);
istream_real& operator>>(fl::u32& n);
istream_real& operator>>(float& f);
istream_real& operator>>(double& d);
// Unified handler for fl:: namespace size-like unsigned integer types to avoid conflicts
// This only handles fl::size and fl::u16 from the fl:: namespace
template<typename T>
typename fl::enable_if<
fl::is_same<T, fl::size>::value ||
fl::is_same<T, fl::u16>::value,
istream_real&
>::type operator>>(T& n);
// Get a line from input
istream_real& getline(string& str);
// Get next character
int get();
// Put back a character
istream_real& putback(char c);
// Peek at next character without consuming it
int peek();
};
// Function to get singleton instance of istream_real (for better linker elimination)
istream_real& cin_real();
// Stub istream class that conditionally delegates to istream_real
class istream {
private:
#if SKETCH_HAS_LOTS_OF_MEMORY
istream_real real_stream_;
#endif
public:
istream() = default;
// Check if stream is in good state
bool good() const {
#if SKETCH_HAS_LOTS_OF_MEMORY
return real_stream_.good();
#else
return true; // Always good on memory-constrained platforms
#endif
}
bool fail() const {
#if SKETCH_HAS_LOTS_OF_MEMORY
return real_stream_.fail();
#else
return false; // Never fail on memory-constrained platforms
#endif
}
bool eof() const {
#if SKETCH_HAS_LOTS_OF_MEMORY
return real_stream_.eof();
#else
return true; // Always EOF on memory-constrained platforms
#endif
}
// Clear error state
void clear() {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_.clear();
#endif
}
// Stream input operators
istream& operator>>(string& str) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> str;
#else
// No-op on memory-constrained platforms
str.clear();
#endif
return *this;
}
istream& operator>>(char& c) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> c;
#else
// No-op on memory-constrained platforms
c = '\0';
#endif
return *this;
}
istream& operator>>(fl::i8& n) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> n;
#else
// No-op on memory-constrained platforms
n = 0;
#endif
return *this;
}
istream& operator>>(fl::u8& n) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> n;
#else
// No-op on memory-constrained platforms
n = 0;
#endif
return *this;
}
istream& operator>>(fl::i16& n) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> n;
#else
// No-op on memory-constrained platforms
n = 0;
#endif
return *this;
}
istream& operator>>(fl::i32& n) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> n;
#else
// No-op on memory-constrained platforms
n = 0;
#endif
return *this;
}
istream& operator>>(fl::u32& n) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> n;
#else
// No-op on memory-constrained platforms
n = 0;
#endif
return *this;
}
istream& operator>>(float& f) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> f;
#else
// No-op on memory-constrained platforms
f = 0.0f;
#endif
return *this;
}
istream& operator>>(double& d) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> d;
#else
// No-op on memory-constrained platforms
d = 0.0;
#endif
return *this;
}
// Unified handler for fl:: namespace size-like unsigned integer types to avoid conflicts
template<typename T>
typename fl::enable_if<
fl::is_same<T, fl::size>::value ||
fl::is_same<T, fl::u16>::value,
istream&
>::type operator>>(T& n) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_ >> n;
#else
// No-op on memory-constrained platforms
n = 0;
#endif
return *this;
}
// Get a line from input
istream& getline(string& str) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_.getline(str);
#else
// No-op on memory-constrained platforms
str.clear();
#endif
return *this;
}
// Get next character
int get() {
#if SKETCH_HAS_LOTS_OF_MEMORY
return real_stream_.get();
#else
// No-op on memory-constrained platforms
return -1;
#endif
}
// Put back a character
istream& putback(char c) {
#if SKETCH_HAS_LOTS_OF_MEMORY
real_stream_.putback(c);
#endif
return *this;
}
// Peek at next character without consuming it
int peek() {
#if SKETCH_HAS_LOTS_OF_MEMORY
return real_stream_.peek();
#else
// No-op on memory-constrained platforms
return -1;
#endif
}
};
// Global cin instance for input (now uses the stub)
extern istream cin;
// Template implementation for istream_real
template<typename T>
typename fl::enable_if<
fl::is_same<T, fl::size>::value ||
fl::is_same<T, fl::u16>::value,
istream_real&
>::type istream_real::operator>>(T& n) {
// Use existing fl::u32 parsing logic for both fl::size and fl::u16
// since they're both unsigned integer types that fit in fl::u32
fl::u32 temp;
(*this) >> temp;
n = static_cast<T>(temp);
return *this;
}
} // namespace fl

View File

@@ -0,0 +1,509 @@
#include "fl/json.h"
#include "fl/string.h"
#include "fl/vector.h"
#include "fl/deque.h"
#include "fl/function.h"
#include "fl/sketch_macros.h"
#include "fl/math.h" // For floor function
#include "fl/compiler_control.h"
#include "fl/thread_local.h"
// Define INT16_MIN, INT16_MAX, and UINT8_MAX if not already defined
#ifndef INT16_MIN
#define INT16_MIN (-32768)
#endif
#ifndef INT16_MAX
#define INT16_MAX 32767
#endif
#ifndef UINT8_MAX
#define UINT8_MAX 255
#endif
#if FASTLED_ENABLE_JSON
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING_NULL_DEREFERENCE
#include "third_party/arduinojson/json.h"
FL_DISABLE_WARNING_POP
#endif // FASTLED_ENABLE_JSON
namespace fl {
// Helper function to check if a double can be reasonably represented as a float
// Used for debug logging - may appear unused in release builds
FL_DISABLE_WARNING_PUSH
FL_DISABLE_WARNING(unused-function)
static bool canBeRepresentedAsFloat(double value) {
auto isnan = [](double value) -> bool {
return value != value;
};
// Check for special values
if (isnan(value)) {
return true; // These can be represented as float
}
// Check if the value is within reasonable float range
// Reject values that are clearly beyond float precision (beyond 2^24 for integers)
// or outside the float range
if (fl::fl_abs(value) > 16777216.0) { // 2^24 - beyond which floats lose integer precision
return false;
}
// For values within reasonable range, allow conversion even with minor precision loss
// This handles cases like 300000.14159 which should be convertible to float
// even though it loses some precision
return true;
}
FL_DISABLE_WARNING_POP
JsonValue& get_null_value() {
static ThreadLocal<JsonValue> null_value;
return null_value.access();
}
JsonObject& get_empty_json_object() {
// thread_local JsonObject empty_object;
static ThreadLocal<JsonObject> empty_object;
return empty_object.access();
}
fl::shared_ptr<JsonValue> JsonValue::parse(const fl::string& txt) {
#if !FASTLED_ENABLE_JSON
return fl::make_shared<JsonValue>(fl::string(txt));
#else
// Determine the size of the JsonDocument needed.
FLArduinoJson::JsonDocument doc;
FLArduinoJson::DeserializationError error = FLArduinoJson::deserializeJson(doc, txt.c_str());
if (error) {
FL_WARN("JSON parsing failed: " << error.c_str());
return fl::make_shared<JsonValue>(nullptr); // Return null on error
}
// Helper function to convert FLArduinoJson::JsonVariantConst to fl::Json::JsonValue
struct Converter {
static fl::shared_ptr<JsonValue> convert(const FLArduinoJson::JsonVariantConst& src) {
if (src.isNull()) {
return fl::make_shared<JsonValue>(nullptr);
} else if (src.is<bool>()) {
return fl::make_shared<JsonValue>(src.as<bool>());
} else if (src.is<int64_t>()) {
// Handle 64-bit integers
return fl::make_shared<JsonValue>(src.as<int64_t>());
} else if (src.is<int32_t>()) {
// Handle 32-bit integers explicitly for platform compatibility
return fl::make_shared<JsonValue>(static_cast<int64_t>(src.as<int32_t>()));
} else if (src.is<uint32_t>()) {
// Handle unsigned 32-bit integers
return fl::make_shared<JsonValue>(static_cast<int64_t>(src.as<uint32_t>()));
} else if (src.is<double>()) {
// Handle double precision floats - convert to float
return fl::make_shared<JsonValue>(static_cast<float>(src.as<double>()));
} else if (src.is<float>()) {
// Handle single precision floats explicitly
return fl::make_shared<JsonValue>(src.as<float>());
} else if (src.is<const char*>()) {
return fl::make_shared<JsonValue>(fl::string(src.as<const char*>()));
} else if (src.is<FLArduinoJson::JsonArrayConst>()) {
FLArduinoJson::JsonArrayConst arr = src.as<FLArduinoJson::JsonArrayConst>();
// Empty arrays should remain regular arrays
if (arr.size() == 0) {
return fl::make_shared<JsonValue>(JsonArray{});
}
// Enum to represent array optimization types
enum ArrayType {
ALL_UINT8,
ALL_INT16,
ALL_FLOATS,
GENERIC_ARRAY
};
// Helper struct to track array type info
struct ArrayTypeInfo {
bool isUint8 = true;
bool isInt16 = true;
bool isFloat = true;
void disableAll() {
isUint8 = false;
isInt16 = false;
isFloat = false;
}
void checkNumericValue(double val) {
// Check integer ranges in one pass
bool isInteger = val == floor(val);
if (!isInteger || val < 0 || val > UINT8_MAX) {
isUint8 = false;
}
if (!isInteger || val < INT16_MIN || val > INT16_MAX) {
isInt16 = false;
}
if (!canBeRepresentedAsFloat(val)) {
isFloat = false;
}
}
void checkIntegerValue(int64_t val) {
// Check all ranges in one pass
if (val < 0 || val > UINT8_MAX) {
isUint8 = false;
}
if (val < INT16_MIN || val > INT16_MAX) {
isInt16 = false;
}
if (val < -16777216 || val > 16777216) {
isFloat = false;
}
}
ArrayType getBestType() const {
if (isUint8) return ALL_UINT8;
if (isInt16) return ALL_INT16;
if (isFloat) return ALL_FLOATS;
return GENERIC_ARRAY;
}
};
ArrayTypeInfo typeInfo;
#if FASTLED_DEBUG_LEVEL >= 2
FASTLED_WARN("Array conversion: processing " << arr.size() << " items");
#endif
for (const auto& item : arr) {
// Check if all items are numeric
if (!item.is<int32_t>() && !item.is<int64_t>() && !item.is<double>()) {
typeInfo.disableAll();
#if FASTLED_DEBUG_LEVEL >= 2
FASTLED_WARN("Non-numeric value found, no optimization possible");
#endif
break;
}
// Update type flags based on item type
if (item.is<double>()) {
double val = item.as<double>();
typeInfo.checkNumericValue(val);
} else {
int64_t val = item.is<int32_t>() ? item.as<int32_t>() : item.as<int64_t>();
typeInfo.checkIntegerValue(val);
}
}
// Determine the optimal array type based on the flags
ArrayType arrayType = arr.size() > 0 ? typeInfo.getBestType() : GENERIC_ARRAY;
// Apply the most appropriate optimization based on determined type
switch (arrayType) {
case ALL_UINT8: {
// All values fit in uint8_t - most compact representation
fl::vector<uint8_t> byteData;
for (const auto& item : arr) {
if (item.is<double>()) {
byteData.push_back(static_cast<uint8_t>(item.as<double>()));
} else {
int64_t val = item.is<int32_t>() ? item.as<int32_t>() : item.as<int64_t>();
byteData.push_back(static_cast<uint8_t>(val));
}
}
return fl::make_shared<JsonValue>(fl::move(byteData));
}
case ALL_INT16: {
// All values fit in int16_t - good compression
fl::vector<int16_t> intData;
for (const auto& item : arr) {
if (item.is<double>()) {
intData.push_back(static_cast<int16_t>(item.as<double>()));
} else {
int64_t val = item.is<int32_t>() ? item.as<int32_t>() : item.as<int64_t>();
intData.push_back(static_cast<int16_t>(val));
}
}
return fl::make_shared<JsonValue>(fl::move(intData));
}
case ALL_FLOATS: {
// All values can be exactly represented as floats - use float vector
fl::vector<float> floatData;
for (const auto& item : arr) {
if (item.is<double>()) {
floatData.push_back(static_cast<float>(item.as<double>()));
} else {
int64_t val = item.is<int32_t>() ? item.as<int32_t>() : item.as<int64_t>();
floatData.push_back(static_cast<float>(val));
}
}
return fl::make_shared<JsonValue>(fl::move(floatData));
}
case GENERIC_ARRAY:
default: {
// No optimization possible - use regular array
JsonArray regularArr;
for (const auto& item : arr) {
regularArr.push_back(convert(item));
}
return fl::make_shared<JsonValue>(fl::move(regularArr));
}
}
} else if (src.is<FLArduinoJson::JsonObjectConst>()) {
JsonObject obj;
for (const auto& kv : src.as<FLArduinoJson::JsonObjectConst>()) {
obj[fl::string(kv.key().c_str())] = convert(kv.value());
}
return fl::make_shared<JsonValue>(fl::move(obj));
}
return fl::make_shared<JsonValue>(nullptr); // Should not happen
}
};
return Converter::convert(doc.as<FLArduinoJson::JsonVariantConst>());
#endif
}
fl::string JsonValue::to_string() const {
// Parse the JSON value to a string, then parse it back to a Json object,
// and use the working to_string_native method
// This is a workaround to avoid reimplementing the serialization logic
// First, create a temporary Json document with this value
Json temp;
// Use the public method to set the value
temp.set_value(fl::make_shared<JsonValue>(*this));
// Use the working implementation
return temp.to_string_native();
}
fl::string Json::to_string_native() const {
if (!m_value) {
return "null";
}
// 🚨 NEW APPROACH: Use fl::deque for memory-efficient JSON serialization
// This avoids memory fragmentation and bypasses broken ArduinoJson integration
fl::deque<char> json_chars;
// Helper lambda for appending strings to deque
auto append_string = [&json_chars](const char* str) {
while (*str) {
json_chars.push_back(*str);
++str;
}
};
// Helper lambda for appending fl::string to deque
auto append_fl_string = [&json_chars](const fl::string& str) {
for (size_t i = 0; i < str.size(); ++i) {
json_chars.push_back(str[i]);
}
};
// Helper lambda for appending escaped strings
auto append_escaped_string = [&](const fl::string& str) {
json_chars.push_back('"');
for (size_t i = 0; i < str.size(); ++i) {
char c = str[i];
switch (c) {
case '"': append_string("\\\""); break;
case '\\': append_string("\\\\"); break;
case '\n': append_string("\\n"); break;
case '\r': append_string("\\r"); break;
case '\t': append_string("\\t"); break;
case '\b': append_string("\\b"); break;
case '\f': append_string("\\f"); break;
default:
json_chars.push_back(c);
break;
}
}
json_chars.push_back('"');
};
// Recursive function to serialize JsonValue to deque
fl::function<void(const JsonValue&)> serialize_value = [&](const JsonValue& value) {
if (value.is_null()) {
append_string("null");
} else if (value.is_bool()) {
auto opt = value.as_bool();
if (opt) {
append_string(*opt ? "true" : "false");
} else {
append_string("null");
}
} else if (value.is_int()) {
auto opt = value.as_int();
if (opt) {
fl::string num_str;
num_str.append(*opt);
append_fl_string(num_str);
} else {
append_string("null");
}
} else if (value.is_float()) {
auto opt = value.as_float();
if (opt) {
fl::string num_str;
// Use fl::string::append which already handles float formatting correctly
//num_str.append(*opt);
num_str.append(static_cast<float>(*opt), 3);
append_fl_string(num_str);
} else {
append_string("null");
}
} else if (value.is_string()) {
auto opt = value.as_string();
if (opt) {
append_escaped_string(*opt);
} else {
append_string("null");
}
} else if (value.is_array()) {
auto opt = value.as_array();
if (opt) {
json_chars.push_back('[');
bool first = true;
for (const auto& item : *opt) {
if (!first) {
json_chars.push_back(',');
}
first = false;
if (item) {
serialize_value(*item);
} else {
append_string("null");
}
}
json_chars.push_back(']');
} else {
append_string("null");
}
} else if (value.is_object()) {
auto opt = value.as_object();
if (opt) {
json_chars.push_back('{');
bool first = true;
for (const auto& kv : *opt) {
if (!first) {
json_chars.push_back(',');
}
first = false;
append_escaped_string(kv.first);
json_chars.push_back(':');
if (kv.second) {
serialize_value(*kv.second);
} else {
append_string("null");
}
}
json_chars.push_back('}');
} else {
append_string("null");
}
} else {
// Handle specialized array types
if (value.is_audio()) {
auto audioOpt = value.as_audio();
if (audioOpt) {
json_chars.push_back('[');
bool first = true;
for (const auto& item : *audioOpt) {
if (!first) {
json_chars.push_back(',');
}
first = false;
fl::string num_str;
num_str.append(static_cast<int>(item));
append_fl_string(num_str);
}
json_chars.push_back(']');
} else {
append_string("null");
}
} else if (value.is_bytes()) {
auto bytesOpt = value.as_bytes();
if (bytesOpt) {
json_chars.push_back('[');
bool first = true;
for (const auto& item : *bytesOpt) {
if (!first) {
json_chars.push_back(',');
}
first = false;
fl::string num_str;
num_str.append(static_cast<int>(item));
append_fl_string(num_str);
}
json_chars.push_back(']');
} else {
append_string("null");
}
} else if (value.is_floats()) {
auto floatsOpt = value.as_floats();
if (floatsOpt) {
json_chars.push_back('[');
bool first = true;
for (const auto& item : *floatsOpt) {
if (!first) {
json_chars.push_back(',');
}
first = false;
fl::string num_str;
num_str.append(static_cast<float>(item), 6);
append_fl_string(num_str);
}
json_chars.push_back(']');
} else {
append_string("null");
}
} else {
append_string("null");
}
}
};
// Serialize the root value
serialize_value(*m_value);
// Convert deque to fl::string efficiently
fl::string result;
if (!json_chars.empty()) {
// Get pointer to the underlying data and construct string directly
result.assign(&json_chars[0], json_chars.size());
}
return result;
}
// Forward declaration for the serializeValue function
fl::string serializeValue(const JsonValue& value);
fl::string Json::normalizeJsonString(const char* jsonStr) {
fl::string result;
if (!jsonStr) {
return result;
}
size_t len = strlen(jsonStr);
for (size_t i = 0; i < len; ++i) {
char c = jsonStr[i];
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
result += c;
}
}
return result;
}
} // namespace fl

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
#include "fl/leds.h"
#include "crgb.h"
#include "fl/assert.h"
#include "fl/xymap.h"
namespace fl {
Leds::Leds(CRGB *leds, const XYMap &xymap) : mXyMap(xymap), mLeds(leds) {}
CRGB &Leds::operator()(int x, int y) {
if (!mXyMap.has(x, y)) {
return empty();
}
return mLeds[mXyMap(x, y)];
}
CRGB &Leds::empty() {
static CRGB empty_led;
return empty_led;
}
const CRGB &Leds::operator()(int x, int y) const {
if (!mXyMap.has(x, y)) {
return empty();
}
return mLeds[mXyMap(x, y)];
}
CRGB *Leds::operator[](int y) {
FASTLED_ASSERT(mXyMap.isSerpentine() || mXyMap.isLineByLine(),
"XYMap is not serpentine or line by line");
return &mLeds[mXyMap(0, y)];
}
const CRGB *Leds::operator[](int y) const {
FASTLED_ASSERT(mXyMap.isSerpentine() || mXyMap.isLineByLine(),
"XYMap is not serpentine or line by line");
return &mLeds[mXyMap(0, y)];
}
Leds::Leds(CRGB *leds, u16 width, u16 height)
: Leds(leds, XYMap::constructRectangularGrid(width, height)) {}
} // namespace fl

View File

@@ -0,0 +1,76 @@
#pragma once
#include "crgb.h"
#include "fl/xymap.h"
namespace fl {
// Leds definition.
// Drawing operations on a block of leds requires information about the layout
// of the leds. Hence this class.
class Leds {
public:
Leds(CRGB *leds, u16 width, u16 height);
Leds(CRGB *leds, const XYMap &xymap);
// Copy constructor and assignment operator.
Leds(const Leds &) = default;
Leds &operator=(const Leds &) = default;
Leds(Leds &&) = default;
// out of bounds access returns empty() led and is safe to read/write.
CRGB &operator()(int x, int y);
const CRGB &operator()(int x, int y) const;
CRGB &at(int x, int y) { return (*this)(x, y); }
const CRGB &at(int x, int y) const { return (*this)(x, y); }
fl::size width() const { return mXyMap.getHeight(); }
fl::size height() const { return mXyMap.getWidth(); }
// Allows normal matrix array (row major) access, bypassing the XYMap.
// Will assert if XYMap is not serpentine or line by line.
CRGB *operator[](int x);
const CRGB *operator[](int x) const;
// Raw data access.
CRGB *rgb() { return mLeds; }
const CRGB *rgb() const { return mLeds; }
const XYMap &xymap() const { return mXyMap; }
operator CRGB *() { return mLeds; }
operator const CRGB *() const { return mLeds; }
void fill(const CRGB &color) {
for (fl::size i = 0; i < mXyMap.getTotal(); ++i) {
mLeds[i] = color;
}
}
protected:
static CRGB &empty(); // Allows safe out of bounds access.
XYMap mXyMap;
CRGB *mLeds;
};
template <fl::size W, fl::size H> class LedsXY : public Leds {
public:
LedsXY() : Leds(mLedsData, XYMap::constructSerpentine(W, H)) {}
explicit LedsXY(bool is_serpentine)
: Leds(mLedsData, is_serpentine ? XYMap::constructSerpentine(W, H)
: XYMap::constructRectangularGrid(W, H)) {}
LedsXY(const LedsXY &) = default;
LedsXY &operator=(const LedsXY &) = default;
void setXyMap(const XYMap &xymap) { mXyMap = xymap; }
void setSerpentine(bool is_serpentine) {
mXyMap = is_serpentine ? XYMap::constructSerpentine(W, H)
: XYMap::constructRectangularGrid(W, H);
}
private:
CRGB mLedsData[W * H] = {};
};
} // namespace fl

View File

@@ -0,0 +1,26 @@
/*
Douglas-Peucker line simplification algorithm.
*/
#include "fl/line_simplification.h"
namespace fl {
namespace /*compiled_test*/ {
// // LineSimplifier<float>::LineSimplifier() : epsilon(0.0) {}
// using LineSimplifierF = LineSimplifier<float>;
// using LineSimplifierD = LineSimplifier<double>;
// LineSimplifierF s_test;
// LineSimplifierD s_testd;
// void foo() {
// fl::vector<vec2<float>> points;
// s_test.simplifyInplace(&points);
// }
} // namespace
} // namespace fl

View File

@@ -0,0 +1,364 @@
#pragma once
/*
Line simplification based of an improved Douglas-Peucker algorithm with only
O(n) extra memory. Memory structures are inlined so that most simplifications
can be done with zero heap allocations.
There are two versions here, one that simplifies using a threshold, and another
version which will simplify to an exact number of points, however the latter is
expensive since it must re-run the algorithm multiple times to find the right
threshold. The first version is much faster and should be used in most cases.
*/
#include "fl/bitset.h"
#include "fl/math.h"
#include "fl/math_macros.h"
#include "fl/pair.h"
#include "fl/point.h"
#include "fl/span.h"
#include "fl/vector.h"
namespace fl {
template <typename NumberT = float> class LineSimplifier {
public:
// This line simplification algorithm will remove vertices that are close
// together upto a distance of mMinDistance. The algorithm is based on the
// Douglas-Peucker but with some tweaks for memory efficiency. Most common
// usage of this class for small sized inputs (~20) will produce no heap
// allocations.
using Point = fl::vec2<NumberT>;
using VectorPoint = fl::vector<Point>;
LineSimplifier() : mMinDistance(EPSILON_F) {}
LineSimplifier(const LineSimplifier &other) = default;
LineSimplifier &operator=(const LineSimplifier &other) = default;
LineSimplifier(LineSimplifier &&other) = default;
LineSimplifier &operator=(LineSimplifier &&other) = default;
explicit LineSimplifier(NumberT e) : mMinDistance(e) {}
void setMinimumDistance(NumberT eps) { mMinDistance = eps; }
// simplifyInPlace.
void simplifyInplace(fl::vector<Point> *polyline) {
simplifyInplaceT(polyline);
}
template <typename VectorType> void simplifyInplace(VectorType *polyLine) {
simplifyInplaceT(polyLine);
}
// simplify to the output vector.
void simplify(const fl::span<const Point> &polyLine,
fl::vector<Point> *out) {
simplifyT(polyLine, out);
}
template <typename VectorType>
void simplify(const fl::span<Point> &polyLine, VectorType *out) {
simplifyT(polyLine, out);
}
template <typename VectorType>
static void removeOneLeastError(VectorType *_poly) {
bitset<256> keep;
VectorType &poly = *_poly;
keep.assign(poly.size(), 1);
const int n = poly.size();
NumberT bestErr = INFINITY_DOUBLE;
int bestIdx = -1;
// scan all interior “alive” points
for (int i = 1; i + 1 < n; ++i) {
if (!keep[i])
continue;
// find previous alive
int L = i - 1;
while (L >= 0 && !keep[L])
--L;
// find next alive
int R = i + 1;
while (R < n && !keep[R])
++R;
if (L < 0 || R >= n)
continue; // endpoints
// compute perpdistance² to the chord L→R
NumberT dx = poly[R].x - poly[L].x;
NumberT dy = poly[R].y - poly[L].y;
NumberT vx = poly[i].x - poly[L].x;
NumberT vy = poly[i].y - poly[L].y;
NumberT len2 = dx * dx + dy * dy;
NumberT err =
(len2 > NumberT(0))
? ((dx * vy - dy * vx) * (dx * vy - dy * vx) / len2)
: (vx * vx + vy * vy);
if (err < bestErr) {
bestErr = err;
bestIdx = i;
}
}
// now “remove” that one point
if (bestIdx >= 0)
// keep[bestIdx] = 0;
poly.erase(poly.begin() + bestIdx);
}
private:
template <typename VectorType> void simplifyInplaceT(VectorType *polyLine) {
// run the simplification algorithm
span<Point> slice(polyLine->data(), polyLine->size());
simplifyT(slice, polyLine);
}
template <typename VectorType>
void simplifyT(const fl::span<const Point> &polyLine, VectorType *out) {
// run the simplification algorithm
simplifyInternal(polyLine);
// copy the result to the output slice
out->assign(mSimplified.begin(), mSimplified.end());
}
// Runs in O(n) allocations: one boolarray + one index stack + one output
// vector
void simplifyInternal(const fl::span<const Point> &polyLine) {
mSimplified.clear();
int n = polyLine.size();
if (n < 2) {
if (n) {
mSimplified.assign(polyLine.data(), polyLine.data() + n);
}
return;
}
const NumberT minDist2 = mMinDistance * mMinDistance;
// mark all points as “kept” initially
keep.assign(n, 1);
// explicit stack of (start,end) index pairs
indexStack.clear();
// indexStack.reserve(64);
indexStack.push_back({0, n - 1});
// process segments
while (!indexStack.empty()) {
// auto [i0, i1] = indexStack.back();
auto pair = indexStack.back();
int i0 = pair.first;
int i1 = pair.second;
indexStack.pop_back();
const bool has_interior = (i1 - i0) > 1;
if (!has_interior) {
// no interior points, just keep the endpoints
// keep[i0] = 1;
// keep[i1] = 1;
continue;
}
// find farthest point in [i0+1 .. i1-1]
NumberT maxDist2 = 0;
int split = i0;
for (int i = i0 + 1; i < i1; ++i) {
if (!keep[i])
continue;
NumberT d2 = PerpendicularDistance2(polyLine[i], polyLine[i0],
polyLine[i1]);
// FASTLED_WARN("Perpendicular distance2 between "
// << polyLine[i] << " and " << polyLine[i0]
// << " and " << polyLine[i1] << " is " << d2);
if (d2 > maxDist2) {
maxDist2 = d2;
split = i;
}
}
if (maxDist2 > minDist2) {
// need to keep that split point and recurse on both halves
indexStack.push_back({i0, split});
indexStack.push_back({split, i1});
} else {
// drop all interior points in this segment
for (int i = i0 + 1; i < i1; ++i) {
keep[i] = 0;
}
}
}
// collect survivors
mSimplified.clear();
mSimplified.reserve(n);
for (int i = 0; i < n; ++i) {
if (keep[i])
mSimplified.push_back(polyLine[i]);
}
}
private:
NumberT mMinDistance;
// workspace buffers
fl::bitset<256> keep; // marks which points survive
fl::vector_inlined<fl::pair<int, int>, 64>
indexStack; // manual recursion stack
VectorPoint mSimplified; // output buffer
static NumberT PerpendicularDistance2(const Point &pt, const Point &a,
const Point &b) {
// vector AB
NumberT dx = b.x - a.x;
NumberT dy = b.y - a.y;
// vector AP
NumberT vx = pt.x - a.x;
NumberT vy = pt.y - a.y;
// squared length of AB
NumberT len2 = dx * dx + dy * dy;
if (len2 <= NumberT(0)) {
// A and B coincide — just return squared dist from A to P
return vx * vx + vy * vy;
}
// crossproduct magnitude (AB × AP) in 2D is (dx*vy dy*vx)
NumberT cross = dx * vy - dy * vx;
// |cross|/|AB| is the perpendicular distance; we want squared:
return (cross * cross) / len2;
}
};
template <typename NumberT = float> class LineSimplifierExact {
public:
LineSimplifierExact() = default;
using Point = vec2<NumberT>;
LineSimplifierExact(int count) : mCount(count) {}
void setCount(u32 count) { mCount = count; }
template <typename VectorType = fl::vector<Point>>
void simplifyInplace(VectorType *polyLine) {
return simplify(*polyLine, polyLine);
}
template <typename VectorType = fl::vector<Point>>
void simplify(const fl::span<const Point> &polyLine, VectorType *out) {
if (mCount > polyLine.size()) {
safeCopy(polyLine, out);
return;
} else if (mCount == polyLine.size()) {
safeCopy(polyLine, out);
return;
} else if (mCount < 2) {
fl::vector_fixed<Point, 2> temp;
if (polyLine.size() > 0) {
temp.push_back(polyLine[0]);
}
if (polyLine.size() > 1) {
temp.push_back(polyLine[polyLine.size() - 1]);
}
out->assign(temp.begin(), temp.end());
return;
}
NumberT est_max_dist = estimateMaxDistance(polyLine);
NumberT min = 0;
NumberT max = est_max_dist;
NumberT mid = (min + max) / 2.0f;
while (true) {
// min < max;
auto diff = max - min;
const bool done = (diff < 0.01f);
out->clear();
mLineSimplifier.setMinimumDistance(mid);
mLineSimplifier.simplify(polyLine, out);
fl::size n = out->size();
if (n == mCount) {
return; // we are done
}
// Handle the last few iterations manually. Often the algo will get
// stuck here.
if (n == mCount + 1) {
// Just one more left, so peel it off.
mLineSimplifier.removeOneLeastError(out);
return;
}
if (n == mCount + 2) {
// Just two more left, so peel them off.
mLineSimplifier.removeOneLeastError(out);
mLineSimplifier.removeOneLeastError(out);
return;
}
if (done) {
while (out->size() > mCount) {
// we have too many points, so we need to increase the
// distance
mLineSimplifier.removeOneLeastError(out);
}
return;
}
if (out->size() < mCount) {
max = mid;
} else {
min = mid;
}
mid = (min + max) / 2.0f;
}
}
private:
static NumberT estimateMaxDistance(const fl::span<const Point> &polyLine) {
// Rough guess: max distance between endpoints
if (polyLine.size() < 2)
return 0;
const Point &first = polyLine[0];
const Point &last = polyLine[polyLine.size() - 1];
NumberT dx = last.x - first.x;
NumberT dy = last.y - first.y;
return sqrt(dx * dx + dy * dy);
}
template <typename VectorType>
void safeCopy(const fl::span<const Point> &polyLine, VectorType *out) {
auto *first_out = out->data();
// auto* last_out = first_out + mCount;
auto *other_first_out = polyLine.data();
// auto* other_last_out = other_first_out + polyLine.size();
const bool is_same = first_out == other_first_out;
if (is_same) {
return;
}
auto *last_out = first_out + mCount;
auto *other_last_out = other_first_out + polyLine.size();
const bool is_overlapping =
(first_out >= other_first_out && first_out < other_last_out) ||
(other_first_out >= first_out && other_first_out < last_out);
if (!is_overlapping) {
out->assign(polyLine.data(), polyLine.data() + polyLine.size());
return;
}
// allocate a temporary buffer
fl::vector_inlined<Point, 64> temp;
temp.assign(polyLine.begin(), polyLine.end());
out->assign(temp.begin(), temp.end());
return;
}
u32 mCount = 10;
LineSimplifier<NumberT> mLineSimplifier;
};
} // namespace fl

Some files were not shown because too many files have changed in this diff Show More