feat: add math_wave color strip source type
Lint & Test / test (push) Has been cancelled

Mathematical wave generator that produces per-LED colors from
configurable waveform layers (sine, triangle, sawtooth, square) with
superposition, mapped through a gradient palette. Supports sync clocks,
bindable speed, and up to 8 wave layers.

- Storage model with wave validation and apply_update
- Numpy-vectorized stream with gradient LUT color mapping
- API schemas (create/update/response) and route registration
- Frontend editor with dynamic wave layer rows, gradient picker,
  speed widget, and IconSelect waveform selectors
- i18n in en/ru/zh
This commit is contained in:
2026-04-05 00:41:07 +03:00
parent edc6d27e2e
commit ace24715c8
16 changed files with 977 additions and 5 deletions
+109
View File
@@ -0,0 +1,109 @@
# math_wave Color Strip Source — Implementation Plan
## Overview
A new CSS type that generates LED colors from configurable mathematical wave functions. Each LED position gets a wave value based on spatial position and time, mapped to a color via a gradient palette. Supports multiple superimposed wave layers, sync clocks, and bindable parameters.
## Requirements
- Waveform types: sine, triangle, sawtooth, square
- Parameters per wave layer: waveform, frequency, amplitude, phase, offset
- Global parameters: speed (bindable), gradient_id (color mapping)
- Wave superposition: list of wave layers combined additively
- Spatial dimension: wave value depends on LED position (0.0-1.0) + time
- Sync clock integration for time parameter
- Color mapping: combined wave output (0.0-1.0) mapped through a gradient
## Phase 1: Storage Model
**File: `server/src/wled_controller/storage/color_strip_source.py`**
- Add `MathWaveColorStripSource` dataclass after `GameEventColorStripSource`
- Fields:
- `waves: list` — default `[{"waveform": "sine", "frequency": 1.0, "amplitude": 1.0, "phase": 0.0, "offset": 0.0}]`
- `speed: BindableFloat` — default 1.0
- `gradient_id: Optional[str]` — references Gradient entity
- Implement `to_dict`, `from_dict`, `create_from_kwargs`, `apply_update` (follow `CandlelightColorStripSource` pattern)
- Valid waveforms: `{"sine", "triangle", "sawtooth", "square"}`
- Add `"math_wave": MathWaveColorStripSource` to `_SOURCE_TYPE_MAP`
## Phase 2: Stream Implementation
**File: `server/src/wled_controller/core/processing/math_wave_stream.py`** (new)
- Class `MathWaveColorStripStream(ColorStripStream)` following `CandlelightColorStripStream` pattern
- Key methods: `__init__`, `_update_from_source`, `configure`, `start`, `stop`, `get_latest_colors`, `update_source`, `set_clock`, `set_gradient_store`
- Animation loop (`_animate_loop`):
- Get `t` from clock (if set) or wall clock
- For each LED at normalized position `p = i / (N-1)`:
- Sum all wave layers: `sum += amplitude * waveform(2*pi*frequency*(p + speed*t) + phase) + offset`
- Clamp result to [0.0, 1.0]
- Map per-LED values to RGB via gradient LUT
- Waveform functions (vectorized with numpy):
- `sine`: `0.5 + 0.5 * np.sin(x)`
- `triangle`: `2.0 * np.abs(np.mod(x / (2*pi), 1.0) - 0.5)`
- `sawtooth`: `np.mod(x / (2*pi), 1.0)`
- `square`: `(np.sin(x) >= 0).astype(float)`
- Double-buffering pattern (same as candlelight)
- Use `self.resolve("speed", self._speed)` for bindable speed
- Gradient resolution via `set_gradient_store` pattern
## Phase 3: API Integration
**File: `server/src/wled_controller/api/schemas/color_strip_sources.py`**
- Add `MathWaveCSSResponse`, `MathWaveCSSCreate`, `MathWaveCSSUpdate`
- Add to `ColorStripSourceResponse`, `ColorStripSourceCreate`, `ColorStripSourceUpdate` unions
**File: `server/src/wled_controller/core/processing/color_strip_stream_manager.py`**
- Import `MathWaveColorStripStream`
- Add `"math_wave": MathWaveColorStripStream` to `_SIMPLE_STREAM_MAP`
- Existing `set_gradient_store` and `_inject_clock` injection handles it automatically
## Phase 4: Frontend
**File: `server/src/wled_controller/static/js/types.ts`**
- Add `'math_wave'` to `CSSSourceType` union
**File: `server/src/wled_controller/static/js/core/icons.ts`**
- Add `math_wave: _svg(P.activity)` to `_colorStripTypeIcons`
**File: `server/src/wled_controller/templates/modals/css-editor.html`**
- Add `<option value="math_wave">` to type select
- Add `<div id="css-editor-math-wave-section">` with:
- Gradient picker (EntitySelect)
- Speed (BindableScalarWidget)
- Wave layers list (dynamic rows: waveform IconSelect, frequency/amplitude/phase/offset inputs, add/remove buttons)
**File: `server/src/wled_controller/static/js/features/color-strips.ts`**
- Add to `CSS_TYPE_KEYS`, `CSS_SECTION_MAP`, `CSS_TYPE_SETUP`, `NON_PICTURE_TYPES`
- Add to `clockTypes` array in `saveCSSEditor`
- Add type handler: `load(css)`, `reset()`, `getPayload(name)`
- Add card renderer showing wave count, gradient swatch, speed, clock badge
- Add wave layer management: `_renderMathWaveRow`, `addMathWaveLayer`, `removeMathWaveLayer`
**Files: `en.json`, `ru.json`, `zh.json`**
- Add i18n keys for type name, description, all field labels, waveform names
## Phase 5: Testing
**File: `server/tests/core/test_math_wave_stream.py`** (new)
- Test wave functions produce expected values at known inputs
- Test single wave spatial pattern
- Test wave superposition
- Test gradient color mapping
- Test clock integration
- Test `update_source` hot-update
- Test `configure` auto-sizing
**File: `server/tests/e2e/test_color_strip_flow.py`**
- Add `test_math_wave_crud` to lifecycle tests
**Storage model tests:**
- Test `from_dict` roundtrip
- Test `create_from_kwargs` with valid/invalid waveforms
- Test `apply_update`
- Test `_SOURCE_TYPE_MAP` dispatch
## Risks & Mitigations
- **Wave layer UI complexity** — Follow existing composite layers / game event mappings patterns
- **Performance with many layers** — Vectorize with numpy; cap max wave layers to 8
- **Gradient resolution** — Stream manager already injects `set_gradient_store` automatically
+151
View File
@@ -0,0 +1,151 @@
# music_sync Color Strip Source — Implementation Plan
## Overview
A higher-level music-reactive CSS that provides semantic audio analysis (BPM detection, beat tracking, energy envelope, drop detection, frequency band energy) and multiple visualization modes. Builds on existing `AudioCaptureManager` and `AudioAnalysis` infrastructure. No new external dependencies — uses numpy-only analysis.
## Requirements
- BPM estimation from real-time audio
- Beat onset detection with configurable threshold
- Smoothed RMS energy envelope with attack/release
- Drop detection: energy drops/buildups
- Frequency band energy: bass (20-250 Hz), mid (250-4k Hz), treble (4k-20k Hz)
- Four visualization modes: `pulse_on_beat`, `energy_gradient`, `spectrum_bands`, `strobe_on_drop`
- Uses existing audio engine infrastructure (no new audio capture code)
- No external dependencies beyond numpy
## Phase 1: Music Analysis Engine
**File: `server/src/wled_controller/core/audio/music_analyzer.py`** (new)
### 1.1 `MusicFeatures` dataclass (frozen=True)
- `bpm: float` — estimated BPM (0 if unknown)
- `beat: bool` — beat detected this frame
- `beat_intensity: float` — 0.0-1.0
- `beat_phase: float` — 0.0-1.0 position in beat cycle
- `energy: float` — smoothed RMS 0.0-1.0
- `energy_delta: float` — rate of change
- `bass_energy, mid_energy, treble_energy: float` — 0.0-1.0
- `drop_state: str` — "idle"|"buildup"|"drop"|"recovery"
- `drop_intensity: float` — 0.0-1.0
### 1.2 `MusicAnalyzer` class
- **State**: rolling energy buffer (~4s at ~43 Hz = ~172 samples), beat history timestamps (last 30), smoothed band energies, BPM estimate, drop state machine
- **`update(analysis: AudioAnalysis) -> MusicFeatures`**: main entry point
- **BPM estimation**: Track beat timestamps, compute median inter-beat interval, exponential smoothing, clamp 40-220 BPM
- **Beat tracking**: Pass through `AudioAnalysis.beat` + compute `beat_phase` (position in current beat cycle)
- **Energy envelope**: Smoothed RMS with configurable attack/release
- **Drop detection**: State machine: `idle -> buildup` (energy rising steadily 1-2s), `buildup -> drop` (energy drops >50% within 100ms), `drop -> recovery` (after 500ms), `recovery -> idle`
- **Frequency bands**: Sum spectrum bins into 3 bands from 64-band spectrum
## Phase 2: Storage Model
**File: `server/src/wled_controller/storage/color_strip_source.py`**
- Add `MusicSyncColorStripSource` dataclass after `AudioColorStripSource`
- Fields:
- `visualization_mode: str` — default `"pulse_on_beat"`
- `audio_source_id: str` — references AudioSource
- `sensitivity: BindableFloat` — default 1.0
- `smoothing: BindableFloat` — default 0.3
- `palette: str` — default `"rainbow"`
- `gradient_id: Optional[str]`
- `color: BindableColor` — primary color
- `color_secondary: BindableColor` — for two-color modes
- `beat_decay: BindableFloat` — default 0.15
- `led_count: int` — 0 = auto-size
- `mirror: bool`
- Add `"music_sync": MusicSyncColorStripSource` to `_SOURCE_TYPE_MAP`
## Phase 3: Stream Implementation
**File: `server/src/wled_controller/core/processing/music_sync_stream.py`** (new)
- Class `MusicSyncColorStripStream(ColorStripStream)` following `AudioColorStripStream` pattern
- Constructor: accept source + audio_capture_manager + stores. Create `MusicAnalyzer` instance
- `start()`: Acquire audio stream, start background thread
- `stop()`: Release audio stream, stop thread
- `_animate_loop()`:
1. Get `AudioAnalysis` from audio stream
2. Apply audio filter pipeline (if any)
3. Feed to `MusicAnalyzer.update()``MusicFeatures`
4. Dispatch to visualization renderer
5. Double-buffer output
### Visualization Renderers
- **`pulse_on_beat`**: Full-strip flash on beat with exponential decay. Between beats: sine-wave pulsing synced to BPM. Color from palette indexed by beat_intensity.
- **`energy_gradient`**: Maps bass→warm, treble→cool. Overall brightness from energy. Gradient scrolls with beat_phase.
- **`spectrum_bands`**: 3 zones (bass/mid/treble), each fills proportionally to band energy. Mirror mode: bass center, treble edges.
- **`strobe_on_drop`**: Idle=gentle breathing. Buildup=increasing pulse. Drop=rapid strobe at 10 Hz. Recovery=fade back.
## Phase 4: API Integration
**File: `server/src/wled_controller/api/schemas/color_strip_sources.py`**
- Add `MusicSyncCSSResponse`, `MusicSyncCSSCreate`, `MusicSyncCSSUpdate`
- Add to all three union types
**File: `server/src/wled_controller/api/routes/color_strip_sources.py`**
- Import + add to `_RESPONSE_MAP`
**File: `server/src/wled_controller/core/processing/color_strip_stream_manager.py`**
- Add `music_sync` branch in `acquire()` (same pattern as `audio` branch)
- Update `refresh_audio_filter_pipelines()` to include `MusicSyncColorStripStream`
## Phase 5: Frontend
**File: `server/src/wled_controller/static/js/core/icons.ts`**
- Add `music_sync: _svg(P.radio)` icon
**File: `server/src/wled_controller/templates/modals/css-editor.html`**
- Add `<div id="css-editor-music-sync-section">` with:
- Visualization mode selector (IconSelect)
- Audio source dropdown
- Sensitivity, Smoothing, Beat Decay (BindableScalarWidget containers)
- Palette/gradient selector (EntitySelect)
- Primary + Secondary color (BindableColorWidget)
- Mirror checkbox
**File: `server/src/wled_controller/static/js/features/color-strips-music-sync.ts`** (new, extracted)
- Editor logic, widget factories, card renderer
**File: `server/src/wled_controller/static/js/features/color-strips.ts`**
- Register type in `CSS_TYPE_KEYS`, `CSS_SECTION_MAP`, `CSS_TYPE_SETUP`, `NON_PICTURE_TYPES`
- Import and register from `color-strips-music-sync.ts`
**Files: `en.json`, `ru.json`, `zh.json`**
- Add i18n keys for type, visualization modes, all field labels
## Phase 6: Testing
**File: `server/tests/core/audio/test_music_analyzer.py`** (new)
- BPM estimation from regular beats (within 5 BPM accuracy)
- BPM handles no beats gracefully
- Beat phase progression
- Energy envelope attack/release
- Frequency band splitting
- Drop detection state machine transitions
- No false drops on steady signal
**File: `server/tests/core/processing/test_music_sync_stream.py`** (new)
- Stream lifecycle (start/stop)
- Produces valid colors
- Hot-update parameters
- Auto-size from device
- All 4 visualization modes produce valid (n,3) uint8 arrays
- Mirror mode symmetry
**Storage + API tests:**
- `from_dict` roundtrip
- CRUD via test client
## Risks & Mitigations
- **BPM accuracy** — Median-of-recent-IBIs is robust for dance/electronic. For ambient, BPM noisy but `energy_gradient` and `spectrum_bands` don't depend on BPM
- **Drop detection false positives** — Require minimum energy threshold + sustained increase before buildup state
- **Frontend file size** — Extract to `color-strips-music-sync.ts` (following composite/notification pattern)
- **Strobe photosensitivity** — Cap at 10 Hz (below 15-25 Hz danger zone), add UI warning
- **Thread safety** — `MusicAnalyzer` owned exclusively by one stream thread, no shared access
## Dependency Order
`math_wave` should be implemented first (simpler, no audio dependency), then `music_sync`.