| 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 |