GCC Code Coverage Report


Directory: ./
File: overlays/wled/wled.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 389 500 77.8%
Functions: 37 55 67.3%
Branches: 242 480 50.4%

Line Branch Exec Source
1
2 /*
3 * Pixel Overlay Effects for Falcon Player (FPP) ported from WLED
4 *
5 * This file is part of the Falcon Player (FPP) and is Copyright (C)
6 * 2013-2022 by the Falcon Player Developers.
7 *
8 * The Falcon Player (FPP) is free software, and is covered under
9 * multiple Open Source licenses. Please see the included 'LICENSES'
10 * file for descriptions of what files are covered by each license.
11 *
12 * This source file is covered under the LGPL v2.1 as described in the
13 * included LICENSE.LGPL file.
14 */
15
16 #include "fpp-pch.h"
17 #include "log.h"
18
19 #include "../../settings.h"
20 #include "../PixelOverlayModel.h"
21
22 #include <cmath>
23 #include <mutex>
24 #include <math.h>
25 #include <time.h>
26
27 #ifdef HAS_GSTREAMER
28 #include <gst/gst.h>
29 #include <gst/app/gstappsink.h>
30 #endif
31
32 #include "wled.h"
33
34 #include "kiss_fftr.h"
35 #include "../../Warnings.h"
36 #include "../../mediaoutput/GStreamerOut.h"
37 #include "../../WLEDAudioSync.h"
38
39 uint8_t blendingStyle = 0; // effect blending/transitionig style
40
41 uint16_t rand16seed = 0;
42 time_t localTime = time(nullptr);
43 uint8_t randomPaletteChangeTime = 0;
44 bool stateChanged = false;
45 byte lastRandomIndex = 0;
46 char* ledmapNames[WLED_MAX_LEDMAPS] = { nullptr };
47 uint16_t ledMaps = 0xFFFF;
48 std::vector<BusConfig> busConfigs;
49 uint8_t errorFlag = 0;
50
51 uint8_t briS = (128); // default brightness
52 uint8_t bri = (briS); // global brightness (set)
53 uint8_t briOld = (0); // global brightness while in transition loop (previous iteration)
54 uint8_t briT = (0); // global brightness during transition
55 uint8_t briLast = (128); // brightness before turned off. Used for toggle function
56 uint8_t whiteLast = (128); // white channel before turned off. Used for toggle function in ir.cpp
57
58 bool useHarmonicRandomPalette = true;
59 bool useGlobalLedBuffer = false;
60 uint8_t realtimeMode = 0;
61 bool realtimeRespectLedMaps = true;
62
63 char settingsPIN[5] = ""; // PIN for settings pages
64 bool correctPIN = 0;
65 unsigned long lastEditTime = 0;
66
67 FakeFL FastLED;
68 Bus BusManager::bus;
69 WLEDFileSystem WLED_FS;
70
71 142 int hour(time_t t) {
72 struct tm tm;
73 142 localtime_r(&t, &tm);
74 142 return tm.tm_hour;
75 }
76 18 int minute(time_t t) {
77 struct tm tm;
78 18 localtime_r(&t, &tm);
79 18 return tm.tm_min;
80 }
81 142 int second(time_t t) {
82 struct tm tm;
83 142 localtime_r(&t, &tm);
84 142 return tm.tm_sec;
85 }
86 18 int day(time_t t) {
87 struct tm tm;
88 18 localtime_r(&t, &tm);
89 18 return tm.tm_mday;
90 }
91 // TimeLib semantics, which the WLED effects were written against: month()
92 // is 1-12, year() the full year, weekday() 1-7 with Sunday = 1. These
93 // returned tm_mon and tm_year as-is, so January printed "Err" and #YYYY "126".
94 18 int month(time_t t) {
95 struct tm tm;
96 18 localtime_r(&t, &tm);
97 18 return tm.tm_mon + 1;
98 }
99 18 int year(time_t t) {
100 struct tm tm;
101 18 localtime_r(&t, &tm);
102 18 return tm.tm_year + 1900;
103 }
104 62 int weekday(time_t t) {
105 struct tm tm;
106 62 localtime_r(&t, &tm);
107 62 return tm.tm_wday + 1;
108 }
109 static const char* monthShortNames_P[] = { "Err", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
110 static const char* monthNames_P[] = { "Err", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
111 // Index 0 is "Err"; anything outside the table maps there too rather than
112 // handing sprintf a pointer from past the end of the array.
113 ✗ const char* monthStr(uint8_t m) {
114 ✗ return monthNames_P[m <= 12 ? m : 0];
115 }
116 18 const char* monthShortStr(uint8_t m) {
117
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 return monthShortNames_P[m <= 12 ? m : 0];
118 }
119
120 static const char* datStrShort_P[] = { "Err", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
121 static const char* datStr_P[] = { "Err", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
122 ✗ const char* dayStr(uint8_t day) {
123 ✗ return datStr_P[day <= 7 ? day : 0];
124 }
125 62 const char* dayShortStr(uint8_t day) {
126
1/2
✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
62 return datStrShort_P[day <= 7 ? day : 0];
127 }
128
129 ✗ uint16_t XY(uint8_t x, uint8_t y) {
130 ✗ return x;
131 }
132 128508 uint32_t get_millisecond_timer() {
133 128508 return GetMonotonicTimeMS();
134 }
135
136 // RMS average
137 ✗ static float fftAddAvgRMS(int from, int to, float* samples) {
138 ✗ double result = 0.0;
139 ✗ for (int i = from; i <= to; i++) {
140 ✗ result += samples[i] * samples[i];
141 }
142 ✗ return sqrtf(result / float(to - from + 1));
143 }
144
145 ✗ static float fftAddAvg(int from, int to, float* samples) {
146 ✗ if (from == to)
147 ✗ return samples[from]; // small optimization
148 ✗ return fftAddAvgRMS(from, to, samples); // use SMS
149 }
150
151 /*
152 static void GenerateSinWave(std::array<float, NUM_SAMPLES> &samples, float sampleRate, float frequency = 440)
153 {
154 float sampleDurationSeconds = 1.0 / sampleRate;
155 for (int i = 0; i < NUM_SAMPLES; ++i) {
156 double sampleTime = (float)(i) * sampleDurationSeconds;
157 samples[i] = std::sin(2.0 * 3.14159 * frequency * sampleTime);
158 }
159 }
160 void HammingWindow(std::array<float, NUM_SAMPLES> &samples)
161 {
162 static const double a0 = 25.0 / 46.0;
163 static const double a1 = 1 - a0;
164 for (int i = 0; i < NUM_SAMPLES; ++i)
165 samples[i] *= a0 - a1 * cos((2.0 * 3.14159 * i) / NUM_SAMPLES);
166 }
167 */
168
169 class WLEDAudioReactiveSoundSource {
170 public:
171 2346 WLEDAudioReactiveSoundSource() {
172 2346 }
173 458 ~WLEDAudioReactiveSoundSource() {
174 #ifdef HAS_GSTREAMER
175
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
458 if (gstPipeline) {
176 ✗ gst_element_set_state(gstPipeline, GST_STATE_NULL);
177 ✗ gst_object_unref(gstPipeline);
178 ✗ gstPipeline = nullptr;
179 }
180 #endif
181 458 }
182 static void InputAudioCallback(WLEDAudioReactiveSoundSource* p, uint8_t* stream, int len) {
183 int16_t* data = (int16_t*)stream;
184 int numSamples = len / sizeof(int16_t);
185 int start = 0;
186 if (numSamples > NUM_SAMPLES) {
187 numSamples = NUM_SAMPLES;
188 start = numSamples - NUM_SAMPLES;
189 }
190 p->inputSamples.fill(0);
191 for (int i = 0; i < numSamples; i++) {
192 p->inputSamples[i] = data[start + i] / 32768.0f;
193 }
194 }
195
196 #ifdef HAS_GSTREAMER
197 // Open a PipeWire audio source via GStreamer pipewiresrc.
198 // dev is the PipeWire node.name (e.g. "alsa_input.usb-0d8c_USB_Sound_Device-00.analog-stereo")
199 1 void openGStreamerCapture(const std::string& dev) {
200
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 GStreamerOutput::EnsureGStreamerInit();
201
202 // Build pipeline: pipewiresrc target-object=<node> ! audioconvert ! audioresample !
203 // audio/x-raw,format=S16LE,channels=1,rate=44100 ! appsink name=wledsink
204 // The node name is set as a property after parsing, never pasted into
205 // the description: it comes from the WLEDAudioInput setting (writable
206 // over the network), and pasted in, "x ! filesrc location=... ! ..."
207 // added elements that read or wrote any file as root.
208 std::string pipelineStr =
209 "pipewiresrc name=wledsrc ! audioconvert ! audioresample ! "
210 "audio/x-raw,format=S16LE,channels=1,rate=44100 ! "
211
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 "appsink name=wledsink max-buffers=2 drop=true";
212
213 1 GError* error = nullptr;
214
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gstPipeline = gst_parse_launch(pipelineStr.c_str(), &error);
215
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (error) {
216 ✗ WarningHolder::AddWarning(52, "WLED Sound Reactive - GStreamer pipeline error: " + std::string(error->message));
217 ✗ LogErr(VB_MEDIAOUT, "WLED GStreamer capture pipeline error: %s\n", error->message);
218 ✗ g_error_free(error);
219 ✗ if (gstPipeline) {
220 ✗ gst_object_unref(gstPipeline);
221 ✗ gstPipeline = nullptr;
222 }
223 ✗ return;
224 }
225
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 if (GstElement* src = gst_bin_get_by_name(GST_BIN(gstPipeline), "wledsrc")) {
226
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 g_object_set(src, "target-object", dev.c_str(), nullptr);
227
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gst_object_unref(src);
228 }
229
230
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 GstElement* appsink = gst_bin_get_by_name(GST_BIN(gstPipeline), "wledsink");
231
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!appsink) {
232 ✗ WarningHolder::AddWarning(52, "WLED Sound Reactive - Could not find appsink in pipeline");
233 ✗ gst_object_unref(gstPipeline);
234 ✗ gstPipeline = nullptr;
235 ✗ return;
236 }
237
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 gst_app_sink_set_emit_signals(GST_APP_SINK(appsink), FALSE);
238 1 gstAppsink = appsink;
239
240 // On the system clock, not the GstPipeWireClock pipewiresrc offers,
241 // as the RTSP audio bridge and Opus sender do: that clock reads the
242 // stream's driver position and stops advancing when the stream's
243 // time reports no rate, and the appsink waits on it before taking
244 // each buffer, so audio sync sent nothing more and reactive effects
245 // went still.
246
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 GstClock* sysClock = gst_system_clock_obtain();
247
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 gst_pipeline_use_clock(GST_PIPELINE(gstPipeline), sysClock);
248
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gst_object_unref(sysClock);
249
250
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 GstStateChangeReturn ret = gst_element_set_state(gstPipeline, GST_STATE_PLAYING);
251
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (ret == GST_STATE_CHANGE_FAILURE) {
252 ✗ WarningHolder::AddWarning(52, "WLED Sound Reactive - Could not start GStreamer capture pipeline for: " + dev);
253 ✗ LogErr(VB_MEDIAOUT, "WLED GStreamer capture: failed to start pipeline for %s\n", dev.c_str());
254 ✗ gst_object_unref(appsink);
255 ✗ gstAppsink = nullptr;
256 ✗ gst_object_unref(gstPipeline);
257 ✗ gstPipeline = nullptr;
258 ✗ return;
259 }
260
261 1 inputSampleRate = 44100;
262 1 gstCaptureActive = true;
263
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogInfo(VB_MEDIAOUT, "WLED GStreamer capture: opened PipeWire source '%s'\n", dev.c_str());
264
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 }
265
266 370 bool pullGStreamerSamples() {
267
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 370 times.
370 if (!gstAppsink)
268 ✗ return false;
269
270
3/6
✓ Branch 0 taken 370 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 370 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 370 times.
✗ Branch 5 not taken.
370 GstSample* sample = gst_app_sink_try_pull_sample(GST_APP_SINK(gstAppsink), 0);
271
2/2
✓ Branch 0 taken 87 times.
✓ Branch 1 taken 283 times.
370 if (!sample)
272 87 return false;
273
274
1/2
✓ Branch 0 taken 283 times.
✗ Branch 1 not taken.
283 GstBuffer* buffer = gst_sample_get_buffer(sample);
275 GstMapInfo map;
276
2/4
✓ Branch 0 taken 283 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 283 times.
✗ Branch 3 not taken.
283 if (gst_buffer_map(buffer, &map, GST_MAP_READ)) {
277 283 int16_t* data = (int16_t*)map.data;
278 283 int numSamples = map.size / sizeof(int16_t);
279 283 int start = 0;
280
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 283 times.
283 if (numSamples > NUM_SAMPLES) {
281 ✗ start = numSamples - NUM_SAMPLES;
282 ✗ numSamples = NUM_SAMPLES;
283 }
284
1/2
✓ Branch 0 taken 283 times.
✗ Branch 1 not taken.
283 inputSamples.fill(0);
285 // Normalize S16 to +/-1.0. computeAudioFrame() assumes that range
286 // -- the "Playing Media" tap feeds it F32LE samples already scaled
287 // that way. Handing it raw int16 puts every value 32768x too high,
288 // which pins volumeSmth and all 16 FFT bins at 255 and stops
289 // samplePeak ever firing, so reactive effects sit flat.
290
2/2
✓ Branch 0 taken 266247 times.
✓ Branch 1 taken 283 times.
266530 for (int i = 0; i < numSamples; i++) {
291 266247 inputSamples[i] = data[start + i] / 32768.0f;
292 }
293
1/2
✓ Branch 0 taken 283 times.
✗ Branch 1 not taken.
283 gst_buffer_unmap(buffer, &map);
294 }
295
1/2
✓ Branch 0 taken 283 times.
✗ Branch 1 not taken.
283 gst_sample_unref(sample);
296 283 return true;
297 }
298
299 1 void openGStreamerAlsaCapture(const std::string& dev) {
300
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 GStreamerOutput::EnsureGStreamerInit();
301
302 // Build pipeline: alsasrc device=<dev> ! audioconvert ! audioresample !
303 // audio/x-raw,format=S16LE,channels=1,rate=44100 ! appsink
304 // The device is set as a property, not pasted into the description;
305 // see openGStreamerCapture().
306 std::string pipelineStr =
307 "alsasrc name=wledsrc ! audioconvert ! audioresample ! "
308 "audio/x-raw,format=S16LE,channels=1,rate=44100 ! "
309
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 "appsink name=wledsink max-buffers=2 drop=true";
310
311 1 GError* error = nullptr;
312
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gstPipeline = gst_parse_launch(pipelineStr.c_str(), &error);
313
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (error) {
314 ✗ WarningHolder::AddWarning(52, "WLED Sound Reactive - Could not open Audio Input Device: " + dev + " Error: " + std::string(error->message));
315 ✗ LogErr(VB_MEDIAOUT, "WLED GStreamer ALSA capture pipeline error: %s\n", error->message);
316 ✗ g_error_free(error);
317 ✗ if (gstPipeline) {
318 ✗ gst_object_unref(gstPipeline);
319 ✗ gstPipeline = nullptr;
320 }
321 ✗ return;
322 }
323
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 if (GstElement* src = gst_bin_get_by_name(GST_BIN(gstPipeline), "wledsrc")) {
324
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 g_object_set(src, "device", dev.c_str(), nullptr);
325
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gst_object_unref(src);
326 }
327
328
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 GstElement* appsink = gst_bin_get_by_name(GST_BIN(gstPipeline), "wledsink");
329
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!appsink) {
330 ✗ WarningHolder::AddWarning(52, "WLED Sound Reactive - Could not find appsink in pipeline");
331 ✗ gst_object_unref(gstPipeline);
332 ✗ gstPipeline = nullptr;
333 ✗ return;
334 }
335
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 gst_app_sink_set_emit_signals(GST_APP_SINK(appsink), FALSE);
336 1 gstAppsink = appsink;
337
338
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 GstStateChangeReturn ret = gst_element_set_state(gstPipeline, GST_STATE_PLAYING);
339
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (ret == GST_STATE_CHANGE_FAILURE) {
340
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 WarningHolder::AddWarning(52, "WLED Sound Reactive - Could not start ALSA capture for: " + dev);
341
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_MEDIAOUT, "WLED GStreamer ALSA capture: failed to start pipeline for %s\n", dev.c_str());
342
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gst_object_unref(appsink);
343 1 gstAppsink = nullptr;
344
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gst_object_unref(gstPipeline);
345 1 gstPipeline = nullptr;
346 1 return;
347 }
348
349 ✗ inputSampleRate = 44100;
350 ✗ gstCaptureActive = true;
351 ✗ LogInfo(VB_MEDIAOUT, "WLED GStreamer ALSA capture: opened device '%s'\n", dev.c_str());
352
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 }
353 #endif
354
355 2 void openAudioDevice(const std::string& dev) {
356 #ifdef HAS_GSTREAMER
357 // In PipeWire mode, dev is a PipeWire node.name — use GStreamer pipewiresrc
358
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (isPipeWireBackend()) {
359 1 openGStreamerCapture(dev);
360 1 return;
361 }
362 // ALSA mode — use GStreamer alsasrc
363 1 openGStreamerAlsaCapture(dev);
364 #else
365 WarningHolder::AddWarning(52, "WLED Sound Reactive - Audio capture requires GStreamer");
366 #endif
367 }
368
369 3825 bool getAudioSamples(std::array<float, NUM_SAMPLES>& samples, int& sampleRate) {
370 // Called from the overlay effect thread (reactive effects) and the
371 // WLEDAudioSync thread: both opened the capture on first use (one
372 // pipeline leaked) and filled inputSamples at once. Every pull is
373 // non-blocking, so holding this over the call costs little.
374
1/2
✓ Branch 0 taken 3825 times.
✗ Branch 1 not taken.
3825 std::lock_guard<std::mutex> lk(sourceLock);
375
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 3810 times.
3825 if (sourceType == -1) {
376
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::string source = getSetting("WLEDAudioInput", "-- Playing Media --");
377
3/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 2 times.
15 if (source == "-- Playing Media --") {
378 13 sourceType = 0;
379 } else {
380 2 sourceType = 1;
381
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 openAudioDevice(source);
382 }
383 15 }
384 3825 bool retValue = false;
385
2/2
✓ Branch 0 taken 3351 times.
✓ Branch 1 taken 474 times.
3825 if (sourceType == 0) {
386 // Playing Media — use GStreamer
387 #ifdef HAS_GSTREAMER
388
1/2
✓ Branch 0 taken 3351 times.
✗ Branch 1 not taken.
3351 retValue = GStreamerOutput::GetAudioSamples(&samples[0], NUM_SAMPLES, sampleRate);
389 #endif
390
1/2
✓ Branch 0 taken 474 times.
✗ Branch 1 not taken.
474 } else if (sourceType == 1) {
391 // Audio Input — GStreamer capture (PipeWire or ALSA)
392 #ifdef HAS_GSTREAMER
393
2/2
✓ Branch 0 taken 370 times.
✓ Branch 1 taken 104 times.
474 if (gstCaptureActive) {
394
1/2
✓ Branch 0 taken 370 times.
✗ Branch 1 not taken.
370 retValue = pullGStreamerSamples();
395
2/2
✓ Branch 0 taken 283 times.
✓ Branch 1 taken 87 times.
370 if (retValue) {
396 283 samples = inputSamples;
397 283 sampleRate = inputSampleRate;
398 }
399 370 return retValue;
400 }
401 #endif
402 }
403 3455 return retValue;
404 3825 }
405
406 std::mutex sourceLock;
407 int sourceType = -1;
408 std::array<float, NUM_SAMPLES> inputSamples;
409 int inputSampleRate = 44100;
410 #ifdef HAS_GSTREAMER
411 GstElement* gstPipeline = nullptr;
412 GstElement* gstAppsink = nullptr;
413 bool gstCaptureActive = false;
414 #endif
415
416 } WLED_SOUND_SOURCE;
417
418 // Free-function entry points (declared in wled.h) so external callers
419 // — notably WLEDAudioSync — can pull a sample buffer and run the same
420 // FFT pipeline the WLED reactive effects use, without holding a
421 // WS2812FXExt instance.
422 2882 bool fetchAudioSamples(std::array<float, NUM_SAMPLES>& samples, int& sampleRate) {
423 2882 return WLED_SOUND_SOURCE.getAudioSamples(samples, sampleRate);
424 }
425
426 2470 void computeAudioFrame(const std::array<float, NUM_SAMPLES>& samples,
427 int sampleRate,
428 AudioFrame& out) {
429 // The FFT config (with its scratch buffer) and the volume smoothing below
430 // are statics, used by every strip's service() and by the WLED Audio
431 // Sync sender on its own thread; concurrent calls corrupted each other's
432 // spectrum. Recursive: service() already holds it.
433
2/4
✓ Branch 0 taken 2470 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2470 times.
✗ Branch 3 not taken.
2470 std::lock_guard<std::recursive_mutex> l(WS2812FXExt::lock());
434
4/8
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2466 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
2470 static kiss_fftr_cfg cfg = kiss_fftr_alloc(NUM_SAMPLES, false, 0, 0);
435 kiss_fft_cpx fft_out[NUM_SAMPLES];
436
1/2
✓ Branch 0 taken 2470 times.
✗ Branch 1 not taken.
2470 kiss_fftr(cfg, (kiss_fft_scalar*)samples.data(), fft_out);
437
438 // Log-bin into 16 octave-ish buckets, mirroring WLED's binning. The loop
439 // below fills buckets 0..17 (it stops when it would start an 18th) - at
440 // 44.1/48kHz the top ~20 bins land in 17 - and only 0..15 are reported.
441 2470 std::array<float, 18> res{};
442 2470 float rate = sampleRate;
443 2470 int curBucket = 0;
444 2470 double freqnext = 440.0 * exp2f(((float)((1 + 1) * 8) - 69.0) / 12.0);
445 2470 int end = freqnext * (float)NUM_SAMPLES / rate;
446 2470 float binHzRange = rate / (float)NUM_SAMPLES;
447
448 2470 float maxValue = 0;
449 2470 int maxBin = 0;
450 2470 int maxBucket = 0;
451
2/2
✓ Branch 0 taken 1264640 times.
✓ Branch 1 taken 2470 times.
1267110 for (int bin = 0; bin < (NUM_SAMPLES / 2); ++bin) {
452
2/2
✓ Branch 0 taken 41990 times.
✓ Branch 1 taken 1222650 times.
1264640 if (bin > end) {
453 41990 curBucket++;
454
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41990 times.
41990 if (curBucket == 18) break;
455 41990 freqnext = 440.0 * exp2f(((double)((curBucket + 1) * 8) - 69.0) / 12.0);
456 41990 end = freqnext * (double)NUM_SAMPLES / rate;
457 }
458 1264640 float nv = sqrtf(fft_out[bin].r * fft_out[bin].r + fft_out[bin].i * fft_out[bin].i);
459
2/2
✓ Branch 0 taken 23327 times.
✓ Branch 1 taken 1241313 times.
1264640 if (nv > maxValue) {
460 23327 maxValue = nv;
461 23327 maxBin = bin;
462 23327 maxBucket = curBucket;
463 }
464 1264640 res[curBucket] = std::max(res[curBucket], nv);
465 }
466
467 2470 float maxRV = 0;
468
2/2
✓ Branch 0 taken 39520 times.
✓ Branch 1 taken 2470 times.
41990 for (int x = 0; x < 16; x++) {
469 39520 int v2 = round(log10(res[x]) * 120.0);
470
2/2
✓ Branch 0 taken 256 times.
✓ Branch 1 taken 39264 times.
39520 if (v2 > 255) v2 = 255;
471
2/2
✓ Branch 0 taken 9342 times.
✓ Branch 1 taken 30178 times.
39520 if (v2 < 0) v2 = 0;
472 39520 out.fftResult[x] = static_cast<uint8_t>(v2);
473
2/2
✓ Branch 0 taken 18727 times.
✓ Branch 1 taken 20793 times.
39520 if (res[x] > maxRV) maxRV = res[x];
474 }
475
476 2470 out.FFT_MajorPeak = (maxBin * binHzRange) + (binHzRange / 2);
477 2470 out.FFT_Magnitude = maxRV;
478
479 // RMS / peak amplitude for the volume fields.
480 2470 float peakAmp = 0.0f;
481
2/2
✓ Branch 0 taken 2529280 times.
✓ Branch 1 taken 2470 times.
2531750 for (int i = 0; i < NUM_SAMPLES; ++i) {
482 2529280 float a = std::fabs(samples[i]);
483
2/2
✓ Branch 0 taken 69458 times.
✓ Branch 1 taken 2459822 times.
2529280 if (a > peakAmp) peakAmp = a;
484 }
485 2470 float instantVolume = std::min(255.0f, peakAmp * 255.0f);
486
487 static float smoothedVolume = 0.0f;
488 2470 smoothedVolume = 0.7f * smoothedVolume + 0.3f * instantVolume;
489
490 // Beat detection — same approach as the WLED audioreactive usermod:
491 // an above-threshold step over the smoothed baseline marks a beat,
492 // held for 50ms with a 100ms refractory window so receivers don't
493 // miss it even with slower poll cadences.
494 static uint64_t lastPeakMs = 0;
495
1/2
✓ Branch 0 taken 2470 times.
✗ Branch 1 not taken.
2470 uint64_t nowMsLocal = GetMonotonicTimeMS();
496 2470 uint8_t peakOut = 0;
497
2/2
✓ Branch 0 taken 2450 times.
✓ Branch 1 taken 20 times.
2470 if (instantVolume > 12.0f &&
498
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 2438 times.
2450 instantVolume > smoothedVolume + 25.0f &&
499
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 8 times.
12 (nowMsLocal - lastPeakMs) >= 100) {
500 4 lastPeakMs = nowMsLocal;
501 }
502
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 2451 times.
2470 if (nowMsLocal - lastPeakMs < 50) peakOut = 1;
503
504 2470 out.volumeSmth = smoothedVolume;
505 2470 out.volumeRaw = static_cast<uint16_t>(instantVolume);
506 2470 out.samplePeak = peakOut;
507 2470 }
508
509 2346 WS2812FXExt::WS2812FXExt() :
510 2346 WS2812FX() {
511
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 pushCurrent(this);
512
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 finalizeInit();
513
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 popCurrent();
514 2346 }
515 1196 WS2812FXExt::WS2812FXExt(PixelOverlayModel* m, int map, int b,
516 uint8_t mode, uint8_t s, uint8_t i, uint8_t pal,
517 uint32_t c1, uint32_t c2, uint32_t c3,
518 uint8_t custom1, uint8_t custom2, uint8_t custom3,
519 int check1, int check2, int check3,
520 const std::string& text,
521 1196 const PolarMode polarMode) :
522 1196 WS2812FX(), model(m), mapping(map), brightness(b) {
523
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 WS2812FXExt::pushCurrent(this);
524
525 // A Radial/Angular mapping asks the model for a polar view of its buffer.
526 // It can decline -- no xLights layout on this player, this model missing
527 // from it, too little geometry -- and then this behaves as Horizontal,
528 // which is what an unrecognised mapping has always done.
529
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 1173 times.
1196 if (polarMode != PolarMode::None) {
530
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 polar = m->getPolarMap(polarMode);
531
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 17 times.
23 if (polar == nullptr) {
532 6 mapping = 0;
533 }
534 }
535 1196 Panel p;
536 int segW, segH;
537
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 1179 times.
1196 if (polar) {
538 // The segment IS the polar buffer: x = radius, y = angle (or the other
539 // way round for the Angular modes). getLength() below must agree with
540 // this or effects index past the LUT.
541 17 segW = polar->width;
542 17 segH = polar->height;
543
4/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1176 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
1179 } else if (mapping == 0 || mapping == 2) {
544 1177 segW = m->getWidth();
545 1177 segH = m->getHeight();
546 } else {
547 2 segW = m->getHeight();
548 2 segH = m->getWidth();
549 }
550 // WLED takes a 1 x N segment as one pixel long; N x 1 is the same to busPixelXY().
551
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1190 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
1196 if (segW == 1 && segH > 1) {
552 6 std::swap(segW, segH);
553 }
554
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 _segments.push_back(Segment(0, segW, 0, segH));
555 1196 p.width = segW;
556 1196 p.height = segH;
557
3/4
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 961 times.
✓ Branch 3 taken 235 times.
1196 isMatrix = segW > 1 && segH > 1;
558
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 _segments[0].setMode(mode, true);
559 1196 _segments[0].speed = s;
560 1196 _segments[0].intensity = i;
561 1196 _segments[0].palette = pal;
562 1196 _segments[0].custom1 = custom1;
563 1196 _segments[0].custom2 = custom2;
564 1196 _segments[0].custom3 = custom3;
565 1196 _segments[0].check1 = check1;
566 1196 _segments[0].check2 = check2;
567 1196 _segments[0].check3 = check3;
568
3/4
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1194 times.
1196 if (text != "") {
569 // Not setName(): that caps the name at WLED_MAX_SEGNAME_LEN, and the
570 // Scrolling Text effect takes its message from here. malloc, not
571 // new[], because ~Segment free()s it.
572 2 _segments[0].name = strdup(text.c_str());
573 }
574
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 _segments[0].refreshLightCapabilities();
575
576
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 _segments[0].setColor(0, c1);
577
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 _segments[0].setColor(1, c2);
578
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 _segments[0].setColor(2, c3);
579
580 1196 um_data.u_size = UDATA_ELEMENTS;
581 1196 um_data.u_type = &udata_types[0];
582 1196 um_data.u_data = &udata_ptrs[0];
583 1196 um_data.u_data[0] = &volumeSmth; //*used (New)
584 1196 um_data.u_type[0] = UMT_FLOAT;
585 1196 um_data.u_data[1] = &volumeRaw; // used (New)
586 1196 um_data.u_type[1] = UMT_UINT16;
587 1196 um_data.u_data[2] = &fftResult[0]; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi)
588 1196 um_data.u_type[2] = UMT_BYTE_ARR;
589 1196 um_data.u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall)
590 1196 um_data.u_type[3] = UMT_BYTE;
591 1196 um_data.u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall)
592 1196 um_data.u_type[4] = UMT_FLOAT;
593 1196 um_data.u_data[5] = &my_magnitude; // used (New)
594 1196 um_data.u_type[5] = UMT_FLOAT;
595 1196 um_data.u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall)
596 1196 um_data.u_type[6] = UMT_BYTE;
597 1196 um_data.u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall)
598 1196 um_data.u_type[7] = UMT_BYTE;
599
600 /*
601 micDataReal = 0.0f;
602 volumeRaw = 0;
603 volumeSmth = 0;
604 sampleAgc = 0;
605 sampleAvg = 0;
606 sampleRaw = 0;
607 rawSampleAgc = 0;
608 my_magnitude = 0;
609 FFT_Magnitude = 0;
610 FFT_MajorPeak = 1;
611 multAgc = 1;
612 // reset FFT data
613 memset(fftCalc, 0, sizeof(fftCalc));
614 memset(fftAvg, 0, sizeof(fftAvg));
615 memset(fftResult, 0, sizeof(fftResult));
616 for (int i = (init ? 0 : 1); i < NUM_GEQ_CHANNELS; i += 2)
617 fftResult[i] = 16; // make a tiny pattern
618 inputLevel = 128;
619 autoResetPeak();
620 */
621
622
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 panel.push_back(p);
623 // Already current (pushed at the top). This used to push a second time,
624 // which made the strip its own parent: the two pops that followed (here
625 // and in the caller) left it current on this thread forever, dangling
626 // once the effect was deleted, and clearInstance() then looped on it.
627
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 finalizeInit();
628 1196 ownMaxWidth = Segment::maxWidth;
629 1196 ownMaxHeight = Segment::maxHeight;
630
1/2
✓ Branch 0 taken 1196 times.
✗ Branch 1 not taken.
1196 popCurrent();
631 1196 }
632
633 1628 WS2812FXExt::~WS2812FXExt() {
634 1628 um_data.u_type = nullptr;
635 1628 um_data.u_data = nullptr;
636 1628 }
637
638 thread_local WS2812FXExt* currentStrip = nullptr;
639 15768483 WS2812FXExt* WS2812FXExt::getInstance() {
640 15768483 WS2812FXExt* cs = currentStrip;
641
2/2
✓ Branch 0 taken 2346 times.
✓ Branch 1 taken 15766137 times.
15768483 if (cs == nullptr) {
642
2/6
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2346 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2346 cs = new WS2812FXExt();
643 2346 currentStrip = cs;
644 }
645 15768483 return cs;
646 }
647 458 void WS2812FXExt::clearInstance() {
648
2/2
✓ Branch 0 taken 458 times.
✓ Branch 1 taken 458 times.
916 while (currentStrip) {
649 458 WS2812FXExt* e = currentStrip;
650 458 currentStrip = currentStrip->parent;
651
1/2
✓ Branch 0 taken 458 times.
✗ Branch 1 not taken.
458 delete e;
652 }
653 458 }
654
655 13387 std::recursive_mutex& WS2812FXExt::lock() {
656 static std::recursive_mutex m;
657 13387 return m;
658 }
659
660 12093 void WS2812FXExt::pushCurrent(WS2812FXExt* e) {
661
1/2
✓ Branch 0 taken 12093 times.
✗ Branch 1 not taken.
12093 if (e) {
662 12093 e->parent = currentStrip;
663 12093 currentStrip = e;
664
2/2
✓ Branch 0 taken 8551 times.
✓ Branch 1 taken 3542 times.
12093 if (e->ownMaxWidth) {
665 // Another strip's construction may have reset the shared matrix
666 // geometry since this one was built; setPixelColorXY() indexes
667 // with it.
668 8551 Segment::maxWidth = e->ownMaxWidth;
669 8551 Segment::maxHeight = e->ownMaxHeight;
670 }
671 ✗ } else if (currentStrip) {
672 ✗ currentStrip = currentStrip->parent;
673 }
674 12093 }
675 12093 void WS2812FXExt::popCurrent() {
676
1/2
✓ Branch 0 taken 12093 times.
✗ Branch 1 not taken.
12093 if (currentStrip) {
677 12093 currentStrip = currentStrip->parent;
678 }
679 12093 }
680
681 6059 PixelOverlayModel* currentBusModel() {
682
1/2
✓ Branch 0 taken 6059 times.
✗ Branch 1 not taken.
6059 return currentStrip ? currentStrip->model : nullptr;
683 }
684
685 // RGBW capability is per-overlay-model, since FPP overlay models can be
686 // RGB (3bpp) or RGBW (4bpp). Whichever effect happens to be running
687 // queries the current strip's underlying model to decide.
688 6059 bool Bus::hasWhite() const {
689 6059 auto* m = currentBusModel();
690
4/4
✓ Branch 0 taken 3713 times.
✓ Branch 1 taken 2346 times.
✓ Branch 2 taken 1323 times.
✓ Branch 3 taken 2390 times.
6059 return m && m->getBytesPerPixel() == 4;
691 }
692
693 // On RGBW models, default to AUTO_BRIGHTER so every existing WLED
694 // effect benefits from the white channel without needing to know
695 // about W. Effects that DO write W explicitly (Slow Transition's
696 // W(SEGCOLOR(0)) reads, future RGBW-aware additions) still work
697 // because the auto-white pass only fills W when the effect left it
698 // at zero.
699 1321 uint8_t Bus::getAutoWhiteMode() const {
700
1/2
✓ Branch 0 taken 1321 times.
✗ Branch 1 not taken.
1321 return hasWhite() ? RGBW_MODE_AUTO_BRIGHTER : RGBW_MODE_MANUAL_ONLY;
701 }
702
703 8280 int Bus::getLength() const {
704 // Must match the Segment the WS2812FXExt constructor built, or an effect
705 // walks off the end of the LUT.
706
2/2
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 8229 times.
8280 if (currentStrip->polar) {
707 51 return (int)currentStrip->polar->indexToCell.size();
708 }
709
2/2
✓ Branch 0 taken 3537 times.
✓ Branch 1 taken 4692 times.
8229 if (currentStrip->model) {
710 3537 return currentStrip->model->getWidth() * currentStrip->model->getHeight();
711 }
712 4692 return 1;
713 }
714 /*
715 * Turn a WLED linear index into a buffer cell of the current model.
716 *
717 * With a polar mapping this is one LUT lookup -- cheaper than the div/mod pair
718 * the ordinary mappings need -- because the ORDER of the LUT already encodes
719 * the layout. Returns false for a polar cell no pixel occupies; polar binning
720 * is inherently sparse and those cells simply are not painted.
721 */
722 3164209 static inline bool busPixelXY(int i, int& x, int& y) {
723 3164209 int w = currentStrip->model->getWidth();
724
725
2/2
✓ Branch 0 taken 8616 times.
✓ Branch 1 taken 3155593 times.
3164209 if (currentStrip->polar) {
726 8616 const PolarBufferMap* pm = currentStrip->polar;
727
3/6
✓ Branch 0 taken 8616 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8616 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 8616 times.
8616 if (i < 0 || (size_t)i >= pm->indexToCell.size()) {
728 ✗ return false;
729 }
730 8616 uint32_t cell = pm->indexToCell[i];
731
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8616 times.
8616 if (cell == PolarBufferMap::Empty) {
732 ✗ return false;
733 }
734 8616 x = cell % w;
735 8616 y = cell / w;
736 8616 return true;
737 }
738
739 3155593 int h = currentStrip->model->getHeight();
740 3155593 int mapping = currentStrip->mapping;
741
4/4
✓ Branch 0 taken 3155113 times.
✓ Branch 1 taken 480 times.
✓ Branch 2 taken 1992 times.
✓ Branch 3 taken 3153121 times.
3155593 if (mapping == 1 || mapping == 3) {
742 2472 y = i % h;
743 2472 x = i / h;
744 } else {
745 3153121 x = i % w;
746 3153121 y = i / w;
747 }
748
749
2/2
✓ Branch 0 taken 648 times.
✓ Branch 1 taken 3154945 times.
3155593 if (mapping == 2) {
750 648 y = h - y - 1;
751 }
752
2/2
✓ Branch 0 taken 1992 times.
✓ Branch 1 taken 3153601 times.
3155593 if (mapping == 3) {
753 1992 x = w - x - 1;
754 }
755 3155593 return true;
756 }
757
758 791136 uint32_t Bus::getPixelColor(int i) const {
759 791136 int x = 0, y = 0;
760
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 791136 times.
791136 if (!busPixelXY(i, x, y)) {
761 ✗ return 0;
762 }
763
764 int r, g, b, wh;
765
1/2
✓ Branch 0 taken 791136 times.
✗ Branch 1 not taken.
791136 currentStrip->model->getOverlayPixelValue(x, y, r, g, b, wh);
766
767 // Undo setPixelColor()'s brightness scale, as WLED does, or effects that
768 // read pixels back apply it again each frame. Rounded up, so a pixel
769 // written back unchanged stores the same value.
770 791136 int brightness = currentStrip->brightness;
771
2/2
✓ Branch 0 taken 350087 times.
✓ Branch 1 taken 441049 times.
791136 if (brightness != 128) {
772
2/2
✓ Branch 0 taken 117862 times.
✓ Branch 1 taken 232225 times.
350087 if (brightness <= 0) {
773 117862 return 0;
774 }
775 // No divide per pixel: ARMv6 (Pi Zero) and the BBB have none in hardware.
776 static thread_local int lutBrightness = -1;
777 static thread_local uint8_t lut[256];
778
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 232221 times.
232225 if (lutBrightness != brightness) {
779
2/2
✓ Branch 0 taken 1024 times.
✓ Branch 1 taken 4 times.
1028 for (int v = 0; v < 256; v++) {
780 1024 lut[v] = std::min((v * 128 + brightness - 1) / brightness, 255);
781 }
782 4 lutBrightness = brightness;
783 }
784 232225 r = lut[r & 0xFF];
785 232225 g = lut[g & 0xFF];
786 232225 b = lut[b & 0xFF];
787 232225 wh = lut[wh & 0xFF];
788 }
789 673274 return (wh << 24) | (r << 16) | (g << 8) | b;
790 }
791 2373073 void Bus::setPixelColor(int i, uint32_t c) {
792 2373073 int x = 0, y = 0;
793
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2373073 times.
2373073 if (!busPixelXY(i, x, y)) {
794 ✗ return;
795 }
796 2373073 int r = R(c);
797 2373073 int g = G(c);
798 2373073 int b = B(c);
799 2373073 int wh = W(c);
800
801 // RGBW auto-white extraction. Almost every WLED effect emits pure
802 // RGB and leaves W=0; on RGBW overlay models we synthesize a W
803 // value from the RGB so the white channel actually drives the
804 // white LED. The bus reports its mode via getAutoWhiteMode():
805 // AUTO_BRIGHTER - W = min(R,G,B), RGB unchanged (brighter overall)
806 // AUTO_ACCURATE - W = min(R,G,B), subtract from RGB (color-faithful)
807 // MAX - W = max(R,G,B), best for white-only LEDs
808 // MANUAL_ONLY - leave W as-is (effect controls it directly)
809
7/8
✓ Branch 0 taken 2372946 times.
✓ Branch 1 taken 127 times.
✓ Branch 2 taken 2372946 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1320 times.
✓ Branch 5 taken 2371626 times.
✓ Branch 6 taken 1320 times.
✓ Branch 7 taken 2371753 times.
2373073 if (wh == 0 && currentStrip->model && currentStrip->model->getBytesPerPixel() == 4) {
810
1/2
✓ Branch 0 taken 1320 times.
✗ Branch 1 not taken.
1320 uint8_t aw = BusManager::bus.getAutoWhiteMode();
811
1/2
✓ Branch 0 taken 1320 times.
✗ Branch 1 not taken.
1320 int mn = std::min({r, g, b});
812
1/2
✓ Branch 0 taken 1320 times.
✗ Branch 1 not taken.
1320 if (aw == RGBW_MODE_AUTO_BRIGHTER) {
813 1320 wh = mn;
814 ✗ } else if (aw == RGBW_MODE_AUTO_ACCURATE) {
815 ✗ wh = mn;
816 ✗ r -= mn; g -= mn; b -= mn;
817 ✗ } else if (aw == RGBW_MODE_MAX) {
818 ✗ wh = std::max({r, g, b});
819 }
820 }
821
822 2373073 int brightness = currentStrip->brightness;
823 2373073 r = r * brightness / 128;
824 2373073 g = g * brightness / 128;
825 2373073 b = b * brightness / 128;
826 2373073 wh = wh * brightness / 128;
827
1/2
✓ Branch 0 taken 2373073 times.
✗ Branch 1 not taken.
2373073 currentStrip->model->setOverlayPixelValue(x, y, min(r, 255), min(g, 255), min(b, 255), min(wh, 255));
828 }
829
830 1441 bool UsermodManager::getUMData(um_data_t** um_data, uint8_t mod_id) {
831 1441 return false;
832 }
833 ✗ CRGB getCRGBForBand(int x, uint8_t* fftResult, int pal) {
834 CRGB value;
835 ✗ CHSV hsv;
836 ✗ if (pal == 71) { // bit hacky to use palette id here, but don't want to litter the code with lots of different methods. TODO: add enum for palette creation type
837 ✗ if (x == 1) {
838 ✗ value = CRGB(fftResult[10] / 2, fftResult[4] / 2, fftResult[0] / 2);
839 ✗ } else if (x == 255) {
840 ✗ value = CRGB(fftResult[10] / 2, fftResult[0] / 2, fftResult[4] / 2);
841 } else {
842 ✗ value = CRGB(fftResult[0] / 2, fftResult[4] / 2, fftResult[10] / 2);
843 }
844 ✗ } else if (pal == 72) {
845 ✗ int b = map(x, 0, 255, 0, 8); // convert palette position to lower half of freq band
846 ✗ hsv = CHSV(fftResult[b], 255, map(fftResult[b], 0, 255, 30, 255)); // pick hue
847 ✗ hsv2rgb_rainbow(hsv, value); // convert to R,G,B
848 }
849 ✗ return value;
850 }
851 ✗ void WS2812FXExt::processSamples(std::array<float, NUM_SAMPLES>& samples, int sampleRate) {
852 // Delegate the FFT + binning + RMS + beat work to the free helper
853 // so WLEDAudioSync's broadcast thread can run the same pipeline
854 // without holding a WS2812FXExt instance. We then copy the result
855 // into our own per-instance um_data fields for FX.cpp to read.
856 ✗ AudioFrame f{};
857 ✗ computeAudioFrame(samples, sampleRate, f);
858
859 ✗ uint8_t* dstFft = (uint8_t*)um_data.u_data[2];
860 ✗ std::memcpy(dstFft, f.fftResult, 16);
861 ✗ FFT_MajorPeak = f.FFT_MajorPeak;
862 ✗ my_magnitude = f.FFT_Magnitude;
863 ✗ volumeSmth = f.volumeSmth;
864 ✗ volumeRaw = f.volumeRaw;
865 ✗ samplePeak = f.samplePeak;
866
867 // maxVol/binNum aren't part of AudioFrame (they're a couple of
868 // decorative scalars only a few effects read); recompute cheaply.
869 ✗ int maxV = 0, maxBucket = 0;
870 ✗ for (int x = 0; x < 16; x++) {
871 ✗ if (f.fftResult[x] > maxV) { maxV = f.fftResult[x]; maxBucket = x; }
872 }
873 ✗ maxVol = maxV;
874 ✗ binNum = maxBucket;
875
876 ✗ if (volumeSmth < 1) my_magnitude = 0.001f;
877 ✗ }
878 // Currently 4 types defined, to be fine tuned and new types added
879 typedef enum UM_SoundSimulations {
880 UMS_BeatSin = 0,
881 UMS_WeWillRockYou,
882 UMS_10_13,
883 UMS_14_3
884 } um_soundSimulations_t;
885 1441 um_data_t* WS2812FXExt::getAudioSamples(int simulationId) {
886 // Network audio source has highest priority — when WLEDAudioSync
887 // is in receive mode and we have a recent packet, drop the local
888 // FFT entirely and use the network values. If no recent packet,
889 // fall through to the normal local-audio path.
890
3/4
✓ Branch 0 taken 1441 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 498 times.
✓ Branch 3 taken 943 times.
1441 if (WLEDAudioSync::INSTANCE.InjectCachedFrame(
891 1441 fftResult.data(), volumeSmth, volumeRaw, samplePeak,
892
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
1441 my_magnitude, FFT_MajorPeak)) {
893 498 return &um_data;
894 }
895
896 std::array<float, NUM_SAMPLES> samples;
897 943 int sampleRate = 0;
898
2/4
✓ Branch 0 taken 943 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 943 times.
943 if (WLED_SOUND_SOURCE.getAudioSamples(samples, sampleRate)) {
899 ✗ processSamples(samples, sampleRate);
900 } else {
901 // arrays
902
1/2
✓ Branch 0 taken 943 times.
✗ Branch 1 not taken.
943 auto ms = GetMonotonicTimeMS();
903
904
2/4
✓ Branch 0 taken 920 times.
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
943 switch (simulationId) {
905 920 default:
906 case UMS_BeatSin:
907
2/2
✓ Branch 0 taken 14720 times.
✓ Branch 1 taken 920 times.
15640 for (int i = 0; i < 16; i++)
908
1/2
✓ Branch 0 taken 14720 times.
✗ Branch 1 not taken.
14720 fftResult[i] = beatsin8_t(120 / (i + 1), 0, 255);
909 // fftResult[i] = (beatsin8_t(120, 0, 255) + (256/16 * i)) % 256;
910 920 volumeSmth = fftResult[8];
911 920 break;
912 23 case UMS_WeWillRockYou:
913
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 22 times.
23 if (ms % 2000 < 200) {
914 1 volumeSmth = hw_random8();
915
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
6 for (int i = 0; i < 5; i++)
916 5 fftResult[i] = hw_random8();
917
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 } else if (ms % 2000 < 400) {
918 ✗ volumeSmth = 0;
919 ✗ for (int i = 0; i < 16; i++)
920 ✗ fftResult[i] = 0;
921
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 } else if (ms % 2000 < 600) {
922 ✗ volumeSmth = hw_random8();
923 ✗ for (int i = 5; i < 11; i++)
924 ✗ fftResult[i] = hw_random8();
925
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 19 times.
22 } else if (ms % 2000 < 800) {
926 3 volumeSmth = 0;
927
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 3 times.
51 for (int i = 0; i < 16; i++)
928 48 fftResult[i] = 0;
929
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 17 times.
19 } else if (ms % 2000 < 1000) {
930 2 volumeSmth = hw_random8();
931
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
12 for (int i = 11; i < 16; i++)
932 10 fftResult[i] = hw_random8();
933 } else {
934 17 volumeSmth = 0;
935
2/2
✓ Branch 0 taken 272 times.
✓ Branch 1 taken 17 times.
289 for (int i = 0; i < 16; i++)
936 272 fftResult[i] = 0;
937 }
938 23 break;
939 ✗ case UMS_10_13:
940 ✗ for (int i = 0; i < 16; i++)
941 ✗ fftResult[i] = perlin8(beatsin8_t(90 / (i + 1), 0, 200) * 15 + (ms >> 10), ms >> 3);
942 ✗ volumeSmth = fftResult[8];
943 ✗ break;
944 ✗ case UMS_14_3:
945 ✗ for (int i = 0; i < 16; i++)
946 ✗ fftResult[i] = perlin8(beatsin8_t(120 / (i + 1), 10, 30) * 10 + (ms >> 14), ms >> 3);
947 ✗ volumeSmth = fftResult[8];
948 ✗ break;
949 }
950
951 943 samplePeak = hw_random8() > 250;
952 943 FFT_MajorPeak = 21 + (volumeSmth * volumeSmth) / 8.0f; // walk thru full range of 21hz...8200hz
953 943 maxVol = 31; // this gets feedback fro UI
954 943 binNum = 8; // this gets feedback fro UI
955 943 volumeRaw = volumeSmth;
956 943 my_magnitude = 10000.0f / 8.0f; // no idea if 10000 is a good value for FFT_Magnitude ???
957
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 876 times.
943 if (volumeSmth < 1)
958 67 my_magnitude = 0.001f; // noise gate closed - mute
959 }
960 943 return &um_data;
961 }
962
963 1441 um_data_t* simulateSound(uint8_t simulationId) {
964 1441 return strip().getAudioSamples(simulationId);
965 }
966