GCC Code Coverage Report


Directory: ./
File: WLEDAudioSync.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 208 216 96.3%
Functions: 17 17 100.0%
Branches: 183 308 59.4%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2026 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds
16
17 #include <arpa/inet.h>
18 #include <errno.h>
19 #include <fcntl.h>
20 #include <ifaddrs.h>
21 #include <net/if.h>
22 #include <netinet/in.h>
23 #include <sys/socket.h>
24 #include <sys/time.h>
25 #include <unistd.h>
26
27 #include <algorithm>
28 #include <chrono>
29 #include <cmath>
30 #include <cstring>
31
32 #include "WLEDAudioSync.h"
33
34 #include "common.h"
35 #include "log.h"
36 #include "settings.h"
37
38 #include "overlays/wled/wled.h" // AudioFrame, fetchAudioSamples, computeAudioFrame
39 #include "SysSocket.h"
40
41 WLEDAudioSync WLEDAudioSync::INSTANCE;
42
43 namespace {
44
45 // WLED V2 audio-sync packet on the wire. 44 bytes, big-endian floats?
46 // No — WLED runs on little-endian ESP32/ESP8266 and emits the bytes
47 // raw, so we transmit native floats. All fields are documented in
48 // /opt/WLED/usermods/audioreactive/audio_reactive.cpp around line 794.
49 #pragma pack(push, 1)
50 struct AudioSyncPacketV2 {
51 char header[6]; // 6 "00002\0"
52 uint8_t reserved1[2]; // 2 unused, present for compiler padding parity
53 float sampleRaw; // 4
54 float sampleSmth; // 4
55 uint8_t samplePeak; // 1 0=no peak, >=1=beat
56 uint8_t reserved2; // 1
57 uint8_t fftResult[16]; // 16 16 freq bins, 0-255 each
58 uint16_t reserved3; // 2
59 float FFT_Magnitude; // 4
60 float FFT_MajorPeak; // 4
61 }; // 44 total
62 #pragma pack(pop)
63 static_assert(sizeof(AudioSyncPacketV2) == 44,
64 "WLED audiosync V2 packet must be exactly 44 bytes");
65
66 constexpr const char* WLED_HEADER_V2 = "00002";
67
68 2943 uint64_t nowMs() {
69 using namespace std::chrono;
70
1/2
✓ Branch 0 taken 2943 times.
✗ Branch 1 not taken.
2943 return duration_cast<milliseconds>(
71 5886 steady_clock::now().time_since_epoch()).count();
72 }
73
74 } // namespace
75
76
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
7038 WLEDAudioSync::WLEDAudioSync() = default;
77
78 458 WLEDAudioSync::~WLEDAudioSync() {
79 458 Cleanup();
80 458 }
81
82 1887 void WLEDAudioSync::Initialize() {
83 // Live-reload: any of the three settings can be edited at runtime
84 // (e.g. via the FPP settings UI) and we'll reconfigure without an
85 // fppd restart. The handler funnels into Reconfigure() which is
86 // mutex-protected against concurrent edits.
87 169 auto cb = [this](const std::string&) { Reconfigure(); };
88
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
9435 registerSettingsListener("WLEDAudioSync", "WLEDAudioSyncMode", cb);
89
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
9275 registerSettingsListener("WLEDAudioSync", "WLEDAudioSyncDest", cb);
90
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
9115 registerSettingsListener("WLEDAudioSync", "WLEDAudioSyncPort", cb);
91
92
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 Reconfigure();
93 1887 }
94
95 2325 void WLEDAudioSync::Cleanup() {
96 // Idempotent: main() calls this during shutdown, and ~WLEDAudioSync() calls
97 // it again at static-destruction time. The second call must be a no-op --
98 // unregisterSettingsListener() reaches into the global SettingsConfig, which
99 // may already be destroyed by then (cross-TU static destruction order),
100 // and locking its destroyed mutex throws EINVAL out of the noexcept dtor.
101
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2325 times.
2325 if (m_cleanedUp.exchange(true)) {
102 ✗ return;
103 }
104
3/6
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2325 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2325 times.
✗ Branch 5 not taken.
9300 unregisterSettingsListener("WLEDAudioSync", "WLEDAudioSyncMode");
105
3/6
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2325 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2325 times.
✗ Branch 5 not taken.
9300 unregisterSettingsListener("WLEDAudioSync", "WLEDAudioSyncDest");
106
3/6
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2325 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2325 times.
✗ Branch 5 not taken.
6975 unregisterSettingsListener("WLEDAudioSync", "WLEDAudioSyncPort");
107 // Under the lock Reconfigure() holds: a settings callback already in
108 // flight could otherwise be starting threads (and opening sockets) while
109 // this stops them. Reconfigure() sees m_cleanedUp and won't restart.
110
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::unique_lock<std::mutex> lock(m_reconfigureLock);
111
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 StopAllThreads();
112 2325 }
113
114 1896 void WLEDAudioSync::Reconfigure() {
115
1/2
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
1896 std::unique_lock<std::mutex> lock(m_reconfigureLock);
116
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1896 times.
1896 if (m_cleanedUp) {
117 ✗ return; // shutting down; Cleanup() has stopped (or will stop) everything
118 }
119
120 1896 Mode newMode = Off;
121
1/2
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
1896 std::string modeStr = getSetting("WLEDAudioSyncMode");
122
9/14
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✓ Branch 3 taken 9 times.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1887 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 1887 times.
✓ Branch 12 taken 9 times.
✓ Branch 13 taken 1887 times.
1896 if (modeStr == "send" || modeStr == "Send" || modeStr == "1") newMode = Send;
123
9/14
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1875 times.
✓ Branch 3 taken 12 times.
✓ Branch 4 taken 1875 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1875 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1875 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 1875 times.
✓ Branch 12 taken 12 times.
✓ Branch 13 taken 1875 times.
1887 else if (modeStr == "receive" || modeStr == "Receive" || modeStr == "2") newMode = Receive;
124
125 1896 int newPort = 11988;
126
1/2
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
1896 int p = getSettingInt("WLEDAudioSyncPort", 0);
127
2/4
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1896 times.
✗ Branch 3 not taken.
1896 if (p > 0 && p < 65536) newPort = p;
128
129
1/2
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
1896 std::string newDest = getSetting("WLEDAudioSyncDest");
130
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1896 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1896 if (newDest.empty()) newDest = "239.0.0.1";
131
132 // Tear down anything currently running before re-applying. Cheap
133 // and unambiguous; sockets reopen in <50 ms.
134
1/2
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
1896 StopAllThreads();
135
136 // Threads are stopped, so nothing can re-raise these — clear any stale
137 // warnings from a previous configuration. The (re)attempt below re-raises
138 // the relevant one if it still fails.
139
3/6
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1896 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1896 times.
✗ Branch 5 not taken.
7584 WarningHolder::RemoveWarning(46, "WLED Audio Sync: could not open the send socket");
140
3/6
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1896 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1896 times.
✗ Branch 5 not taken.
7584 WarningHolder::RemoveWarning(46, "WLED Audio Sync: could not open the receive socket");
141
3/6
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1896 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1896 times.
✗ Branch 5 not taken.
5688 WarningHolder::RemoveWarning(46, "WLED Audio Sync: invalid destination address");
142
143 1896 m_mode = newMode;
144 1896 m_port = newPort;
145
1/2
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
1896 m_destAddr = newDest;
146
147
2/2
✓ Branch 0 taken 1875 times.
✓ Branch 1 taken 21 times.
1896 if (m_mode == Off) {
148
1/2
✓ Branch 0 taken 1875 times.
✗ Branch 1 not taken.
1875 LogInfo(VB_GENERAL, "WLEDAudioSync: disabled\n");
149 1875 return;
150 }
151
152
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 CacheLocalIPs();
153 21 m_running = true;
154
155
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 12 times.
21 if (m_mode == Send) {
156
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
9 if (!OpenSendSocket()) {
157 ✗ LogErr(VB_GENERAL, "WLEDAudioSync: send socket open failed\n");
158 ✗ WarningHolder::AddWarning(46, "WLED Audio Sync: could not open the send socket");
159 ✗ m_mode = Off;
160 ✗ m_running = false;
161 ✗ return;
162 }
163
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
18 m_sendThread = new std::thread([this]() {
164
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 SetThreadName("FPP-WLEDAudio-S");
165 9 SendLoop();
166
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
18 });
167
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 LogInfo(VB_GENERAL, "WLEDAudioSync: SEND mode, %s:%d\n",
168 m_destAddr.c_str(), m_port);
169
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 } else if (m_mode == Receive) {
170
3/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 11 times.
12 if (!OpenReceiveSocket()) {
171
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_GENERAL, "WLEDAudioSync: receive socket open failed\n");
172
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
3 WarningHolder::AddWarning(46, "WLED Audio Sync: could not open the receive socket");
173 1 m_mode = Off;
174 1 m_running = false;
175 1 return;
176 }
177
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
22 m_recvThread = new std::thread([this]() {
178
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 SetThreadName("FPP-WLEDAudio-R");
179 11 ReceiveLoop();
180
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
22 });
181
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 LogInfo(VB_GENERAL, "WLEDAudioSync: RECEIVE mode, port %d\n", m_port);
182 }
183
6/6
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 1876 times.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 1876 times.
✓ Branch 4 taken 20 times.
✓ Branch 5 taken 1876 times.
5648 }
184
185 4221 void WLEDAudioSync::StopAllThreads() {
186 4221 m_running = false;
187
188
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4210 times.
4221 if (m_recvSocket >= 0) {
189 // shutdown() wakes the listener's blocking recvfrom() (same
190 // lesson as the WLED API responder — close alone doesn't).
191 11 ::shutdown(m_recvSocket, SHUT_RDWR);
192 }
193
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4210 times.
4221 if (m_recvThread) {
194
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if (m_recvThread->joinable()) m_recvThread->join();
195
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 delete m_recvThread;
196 11 m_recvThread = nullptr;
197 }
198 // Closed only once the listener has been joined: closed under it, the
199 // descriptor number could be reused by another socket that its next
200 // recvfrom() would then read from.
201
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4210 times.
4221 if (m_recvSocket >= 0) {
202 11 ::close(m_recvSocket);
203 11 m_recvSocket = -1;
204 }
205
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 4212 times.
4221 if (m_sendThread) {
206
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (m_sendThread->joinable()) m_sendThread->join();
207
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 delete m_sendThread;
208 9 m_sendThread = nullptr;
209 }
210
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 4212 times.
4221 if (m_sendSocket >= 0) {
211 9 ::close(m_sendSocket);
212 9 m_sendSocket = -1;
213 }
214 4221 }
215
216 9 bool WLEDAudioSync::OpenSendSocket() {
217 9 m_sendSocket = OpenSocket(AF_INET, SOCK_DGRAM, 0);
218
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (m_sendSocket < 0) return false;
219
220 // Enable broadcast (allows 255.255.255.255 / x.x.x.255 destinations).
221 9 int yes = 1;
222 9 ::setsockopt(m_sendSocket, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes));
223
224 // Multicast TTL of 1 keeps multicast packets link-local; same
225 // semantics WLED uses on its own audiosync transmitter.
226 9 unsigned char ttl = 1;
227 9 ::setsockopt(m_sendSocket, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
228
229 9 return true;
230 }
231
232 12 bool WLEDAudioSync::OpenReceiveSocket() {
233 12 m_recvSocket = OpenSocket(AF_INET, SOCK_DGRAM, 0);
234
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (m_recvSocket < 0) return false;
235
236 12 int yes = 1;
237 12 ::setsockopt(m_recvSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
238 // SO_REUSEPORT intentionally NOT set: it triggers the kernel's
239 // load-balancing path even with a single binder, and on some
240 // kernels causes UDP packets to be silently dropped when the
241 // sender and receiver are both on loopback. SO_REUSEADDR alone
242 // gives us the bind-restart-without-TIME_WAIT semantics we want.
243
244 // Belt and braces for StopAllThreads(): not every platform wakes a
245 // recvfrom() on an unconnected UDP socket at shutdown(), so the
246 // listener also rechecks m_running at least this often.
247 12 struct timeval tv = { 0, 250000 };
248 12 ::setsockopt(m_recvSocket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
249
250 12 sockaddr_in addr{};
251 12 addr.sin_family = AF_INET;
252 12 addr.sin_addr.s_addr = htonl(INADDR_ANY);
253 12 addr.sin_port = htons(m_port);
254
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 11 times.
12 if (::bind(m_recvSocket, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
255
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_GENERAL, "WLEDAudioSync: bind(%d) failed: %s\n",
256 m_port, FPPstrerror(errno));
257
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ::close(m_recvSocket);
258 1 m_recvSocket = -1;
259 1 return false;
260 }
261
262 // If the destination address is multicast, join the group so
263 // multicast packets are delivered to us. Unicast/broadcast needs
264 // no extra setup beyond the bind above.
265 11 in_addr a{};
266 11 if (inet_pton(AF_INET, m_destAddr.c_str(), &a) == 1
267
5/6
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 2 times.
11 && IN_MULTICAST(ntohl(a.s_addr))) {
268 9 ip_mreq mreq{};
269 9 mreq.imr_multiaddr = a;
270 9 mreq.imr_interface.s_addr = htonl(INADDR_ANY);
271 9 if (::setsockopt(m_recvSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP,
272
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 &mreq, sizeof(mreq)) < 0) {
273 ✗ LogWarn(VB_GENERAL, "WLEDAudioSync: IP_ADD_MEMBERSHIP %s failed: %s\n",
274 m_destAddr.c_str(), FPPstrerror(errno));
275 }
276 }
277 11 return true;
278 }
279
280 21 void WLEDAudioSync::CacheLocalIPs() {
281 21 m_localIPs.clear();
282 21 ifaddrs* ifa = nullptr;
283
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (getifaddrs(&ifa) != 0) return;
284
2/2
✓ Branch 0 taken 105 times.
✓ Branch 1 taken 21 times.
126 for (auto* p = ifa; p; p = p->ifa_next) {
285
3/4
✓ Branch 0 taken 105 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✓ Branch 3 taken 42 times.
105 if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue;
286 42 auto* in = reinterpret_cast<sockaddr_in*>(p->ifa_addr);
287
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 m_localIPs.insert(in->sin_addr.s_addr);
288 }
289 21 freeifaddrs(ifa);
290 }
291
292 3680 bool WLEDAudioSync::IsLocalAddress(uint32_t ipv4_n) const {
293 3680 return m_localIPs.count(ipv4_n) > 0;
294 }
295
296 // Standalone broadcast loop. Runs whenever WLEDAudioSync is in Send
297 // mode, independent of any local reactive effect — so FPP can be a
298 // pure audio source for a network of WLED reactive devices even with
299 // no overlay effects active locally. Cadence matches WLED's own
300 // audioreactive transmit rate (10ms / 100Hz).
301 9 void WLEDAudioSync::SendLoop() {
302 9 sockaddr_in dst{};
303 9 dst.sin_family = AF_INET;
304 9 dst.sin_port = htons(m_port);
305
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 7 times.
9 if (inet_pton(AF_INET, m_destAddr.c_str(), &dst.sin_addr) != 1) {
306
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_GENERAL, "WLEDAudioSync: invalid destination '%s'\n",
307 m_destAddr.c_str());
308
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
6 WarningHolder::AddWarning(46, "WLED Audio Sync: invalid destination address");
309 2 return;
310 }
311
312 std::array<float, NUM_SAMPLES> samples;
313 7 AudioFrame frame{};
314
315
2/2
✓ Branch 0 taken 2882 times.
✓ Branch 1 taken 7 times.
2889 while (m_running) {
316 2882 int sampleRate = 0;
317
3/4
✓ Branch 0 taken 2882 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 412 times.
✓ Branch 3 taken 2470 times.
2882 if (!fetchAudioSamples(samples, sampleRate)) {
318 // No audio source producing right now (nothing playing,
319 // mic unconfigured, etc.). Wait a bit and re-check rather
320 // than spinning.
321
1/2
✓ Branch 0 taken 412 times.
✗ Branch 1 not taken.
412 std::this_thread::sleep_for(std::chrono::milliseconds(50));
322 412 continue;
323 }
324
325
1/2
✓ Branch 0 taken 2470 times.
✗ Branch 1 not taken.
2470 computeAudioFrame(samples, sampleRate, frame);
326
327 2470 AudioSyncPacketV2 pkt{};
328 2470 std::memcpy(pkt.header, WLED_HEADER_V2, 5); // header[6] keeps the trailing NUL
329 2470 pkt.sampleRaw = static_cast<float>(frame.volumeRaw);
330 2470 pkt.sampleSmth = frame.volumeSmth;
331 2470 pkt.samplePeak = frame.samplePeak;
332 2470 std::memcpy(pkt.fftResult, frame.fftResult, 16);
333 2470 pkt.FFT_Magnitude = frame.FFT_Magnitude;
334 2470 pkt.FFT_MajorPeak = frame.FFT_MajorPeak;
335
336
1/2
✓ Branch 0 taken 2470 times.
✗ Branch 1 not taken.
2470 ::sendto(m_sendSocket, &pkt, sizeof(pkt), 0,
337 reinterpret_cast<sockaddr*>(&dst), sizeof(dst));
338
339
1/2
✓ Branch 0 taken 2470 times.
✗ Branch 1 not taken.
2470 std::this_thread::sleep_for(std::chrono::milliseconds(10));
340 }
341 }
342
343 11 void WLEDAudioSync::ReceiveLoop() {
344 AudioSyncPacketV2 pkt;
345 uint8_t buf[sizeof(AudioSyncPacketV2) + 1]; // one spare byte: a longer datagram reads 45 and is dropped
346
347 11 sockaddr_in from{};
348 11 socklen_t fromLen = sizeof(from);
349 11 bool firstPacketLogged = false;
350
351
1/2
✓ Branch 0 taken 3795 times.
✗ Branch 1 not taken.
3795 while (m_running) {
352 3795 fromLen = sizeof(from);
353
1/2
✓ Branch 0 taken 3795 times.
✗ Branch 1 not taken.
3795 ssize_t n = ::recvfrom(m_recvSocket, buf, sizeof(buf), 0,
354 reinterpret_cast<sockaddr*>(&from), &fromLen);
355
2/2
✓ Branch 0 taken 115 times.
✓ Branch 1 taken 3680 times.
3795 if (n <= 0) {
356 // Socket closed by Cleanup(), or transient error — bail
357 // out cleanly when we're shutting down.
358
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 104 times.
115 if (!m_running) break;
359 1341 continue;
360 }
361
362 // Loopback suppression: ignore packets from ourselves. Without
363 // this, a node in `send` mode that hears its own multicast back
364 // via host routing would inject its own FFT data on top of the
365 // locally-computed FFT, oscillating.
366
3/4
✓ Branch 0 taken 3680 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 198 times.
✓ Branch 3 taken 3482 times.
3680 if (IsLocalAddress(from.sin_addr.s_addr)) continue;
367
368 // V1 ("00001", 88-byte) packets exist for backwards-compat with
369 // very old WLED firmware; we accept the V2 form only since that's
370 // what every current WLED build emits.
371
2/2
✓ Branch 0 taken 297 times.
✓ Branch 1 taken 3185 times.
3482 if (n != sizeof(AudioSyncPacketV2)) continue;
372 3185 std::memcpy(&pkt, buf, sizeof(pkt));
373
2/2
✓ Branch 0 taken 197 times.
✓ Branch 1 taken 2988 times.
3185 if (std::memcmp(pkt.header, WLED_HEADER_V2, 5) != 0) continue;
374
375 // The floats come from anyone on the LAN and go straight into the
376 // WLED effects: an Inf major peak never leaves Rocktaves'
377 // "while (f > 249) f /= 2" (holding the WLED lock), and NaN or huge
378 // values are undefined in the effects' integer casts. Drop
379 // non-finite packets and clamp the rest to what WLED itself sends.
380 // (Copied out first: the packet is packed, so its floats can't bind
381 // to std::clamp's references.)
382 2988 float sampleRaw = pkt.sampleRaw;
383 2988 float sampleSmth = pkt.sampleSmth;
384 2988 float fftMagnitude = pkt.FFT_Magnitude;
385 2988 float fftMajorPeak = pkt.FFT_MajorPeak;
386
2/2
✓ Branch 0 taken 2790 times.
✓ Branch 1 taken 99 times.
5877 if (!std::isfinite(sampleRaw) || !std::isfinite(sampleSmth) ||
387
8/8
✓ Branch 0 taken 2889 times.
✓ Branch 1 taken 99 times.
✓ Branch 2 taken 2691 times.
✓ Branch 3 taken 99 times.
✓ Branch 4 taken 248 times.
✓ Branch 5 taken 2443 times.
✓ Branch 6 taken 545 times.
✓ Branch 7 taken 2443 times.
5877 !std::isfinite(fftMagnitude) || !std::isfinite(fftMajorPeak)) {
388 545 continue;
389 }
390
1/2
✓ Branch 0 taken 2443 times.
✗ Branch 1 not taken.
2443 sampleRaw = std::clamp(sampleRaw, 0.0f, 65535.0f);
391
1/2
✓ Branch 0 taken 2443 times.
✗ Branch 1 not taken.
2443 sampleSmth = std::clamp(sampleSmth, 0.0f, 255.0f);
392
1/2
✓ Branch 0 taken 2443 times.
✗ Branch 1 not taken.
2443 fftMagnitude = std::clamp(fftMagnitude, 0.0f, 1.0e7f);
393
1/2
✓ Branch 0 taken 2443 times.
✗ Branch 1 not taken.
2443 fftMajorPeak = std::clamp(fftMajorPeak, 1.0f, 11025.0f); // FX.cpp MAX_FREQUENCY
394
395
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2435 times.
2443 if (!firstPacketLogged) {
396 8 char fromBuf[INET_ADDRSTRLEN] = {};
397 8 inet_ntop(AF_INET, &from.sin_addr, fromBuf, sizeof(fromBuf));
398 // Use raw fprintf rather than LogInfo: observed that the
399 // FPPLogger may swallow messages from a brand-new thread on
400 // its very first call.
401
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 fprintf(stderr, "WLEDAudioSync: receiving audio sync from %s\n",
402 fromBuf);
403
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 fflush(stderr);
404 8 firstPacketLogged = true;
405 }
406
407
1/2
✓ Branch 0 taken 2443 times.
✗ Branch 1 not taken.
2443 std::unique_lock<std::mutex> lock(m_cacheLock);
408
1/2
✓ Branch 0 taken 2443 times.
✗ Branch 1 not taken.
2443 m_cacheTimestampMs = nowMs();
409 2443 std::memcpy(m_cachedFFT.data(), pkt.fftResult, 16);
410 2443 m_cachedVolumeSmth = sampleSmth;
411 2443 m_cachedVolumeRaw = static_cast<uint16_t>(sampleRaw);
412 2443 m_cachedSamplePeak = pkt.samplePeak;
413 2443 m_cachedFFTMagnitude = fftMagnitude;
414 2443 m_cachedFFTMajorPeak = fftMajorPeak;
415 2443 }
416 11 }
417
418 1441 bool WLEDAudioSync::InjectCachedFrame(uint8_t fftResult[16],
419 float& volumeSmth,
420 uint16_t& volumeRaw,
421 uint8_t& samplePeak,
422 float& FFT_Magnitude,
423 float& FFT_MajorPeak) {
424
2/2
✓ Branch 0 taken 881 times.
✓ Branch 1 taken 560 times.
1441 if (m_mode != Receive) return false;
425
426
1/2
✓ Branch 0 taken 560 times.
✗ Branch 1 not taken.
560 std::unique_lock<std::mutex> lock(m_cacheLock);
427
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 500 times.
560 if (m_cacheTimestampMs == 0) return false;
428
3/4
✓ Branch 0 taken 500 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 498 times.
500 if (nowMs() - m_cacheTimestampMs > kFrameTtlMs) return false;
429
430 498 std::memcpy(fftResult, m_cachedFFT.data(), 16);
431 498 volumeSmth = m_cachedVolumeSmth;
432 498 volumeRaw = m_cachedVolumeRaw;
433 498 samplePeak = m_cachedSamplePeak;
434 498 FFT_Magnitude = m_cachedFFTMagnitude;
435 498 FFT_MajorPeak = m_cachedFFTMajorPeak;
436 498 return true;
437 560 }
438