| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-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 "AudioLevelMonitor.h" | ||
| 16 | |||
| 17 | #ifdef HAS_AUDIO_LEVEL_MONITOR | ||
| 18 | |||
| 19 | #include <algorithm> | ||
| 20 | #include <chrono> | ||
| 21 | #include <cmath> | ||
| 22 | |||
| 23 | #include "../fpp-json.h" | ||
| 24 | #include "common_mini.h" | ||
| 25 | #include "log.h" | ||
| 26 | |||
| 27 | AudioLevelMonitor AudioLevelMonitor::INSTANCE; | ||
| 28 | |||
| 29 | // How often the level element reports, and therefore the ceiling on how fast a | ||
| 30 | // meter can move. 100ms is well below what reads as laggy and keeps the message | ||
| 31 | // traffic trivial. | ||
| 32 | static constexpr int LEVEL_INTERVAL_NS = 100000000; | ||
| 33 | |||
| 34 | // Floor of the meter scale. Programme material sits well above this; anything | ||
| 35 | // quieter is indistinguishable from silence on a bar a few pixels tall. | ||
| 36 | static constexpr double METER_FLOOR_DB = -60.0; | ||
| 37 | |||
| 38 | 458 | AudioLevelMonitor::~AudioLevelMonitor() { | |
| 39 | 458 | Shutdown(); | |
| 40 | 458 | } | |
| 41 | |||
| 42 | // Builds and starts a meter's pipeline. Never called with m_lock held: it | ||
| 43 | // talks to the PipeWire daemon (pipewiresrc connects as the state rises), and | ||
| 44 | // the status websocket reads levels under m_lock. | ||
| 45 | 13 | bool AudioLevelMonitor::BuildMeter(const std::string& node, bool isSink, Meter& m) { | |
| 46 | // Node names are validated by the callers that accept them from outside | ||
| 47 | // (the HTTP endpoint), but this builds a pipeline description string, so | ||
| 48 | // anything unexpected is a reason to refuse rather than to quote around. | ||
| 49 |
3/6✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 13 times.
|
26 | if (node.empty() || |
| 50 | 13 | node.find_first_not_of("abcdefghijklmnopqrstuvwxyz" | |
| 51 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
| 52 | "0123456789_.-") != std::string::npos) { | ||
| 53 | ✗ | LogWarn(VB_MEDIAOUT, "AudioLevelMonitor: refusing node with unexpected name: %s\n", node.c_str()); | |
| 54 | ✗ | return false; | |
| 55 | } | ||
| 56 | |||
| 57 | // Capturing what a sink is playing needs stream.capture.sink; a playback | ||
| 58 | // stream is captured by targeting it directly and must NOT have it set. | ||
| 59 | // | ||
| 60 | // Getting this wrong is not visibly an error: pipewiresrc cannot resolve | ||
| 61 | // the target, silently falls back to the default sink, and reports a | ||
| 62 | // plausible level for entirely the wrong node. That is what made every | ||
| 63 | // meter read the same signal and move together. (There is no ".monitor" | ||
| 64 | // node to target either -- that name is a PulseAudio compatibility | ||
| 65 | // construct and matches nothing in the PipeWire graph.) | ||
| 66 | // GStreamer may not have been initialised yet: metering can be switched on | ||
| 67 | // before anything has played, and gst_parse_launch() segfaults rather than | ||
| 68 | // failing when it is called first. Same guard AES67Manager and | ||
| 69 | // OpusRTPManager use. | ||
| 70 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
|
13 | if (!gst_is_initialized()) { |
| 71 | ✗ | gst_init(nullptr, nullptr); | |
| 72 | } | ||
| 73 | |||
| 74 | // The stream-properties value is a GstStructure and must be quoted, or the | ||
| 75 | // comma inside it terminates the property and the rest is parsed as another | ||
| 76 | // element. | ||
| 77 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | std::string desc = "pipewiresrc target-object=" + node; |
| 78 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 4 times.
|
13 | if (isSink) { |
| 79 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | desc += " stream-properties=\"props,stream.capture.sink=true\""; |
| 80 | } | ||
| 81 | 13 | desc += " ! audioconvert ! audio/x-raw,channels=1,rate=8000 ! " + | |
| 82 |
3/6✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
|
65 | std::string("level interval=") + std::to_string(LEVEL_INTERVAL_NS) + |
| 83 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
|
13 | " post-messages=true ! fakesink sync=false"; |
| 84 | |||
| 85 | 13 | GError* err = nullptr; | |
| 86 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | GstElement* pipeline = gst_parse_launch(desc.c_str(), &err); |
| 87 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
|
13 | if (!pipeline || err) { |
| 88 | ✗ | LogWarn(VB_MEDIAOUT, "AudioLevelMonitor: could not build meter for %s: %s\n", | |
| 89 | node.c_str(), err ? err->message : "unknown error"); | ||
| 90 | ✗ | if (err) { | |
| 91 | ✗ | g_error_free(err); | |
| 92 | } | ||
| 93 | ✗ | if (pipeline) { | |
| 94 | ✗ | gst_object_unref(pipeline); | |
| 95 | } | ||
| 96 | ✗ | return false; | |
| 97 | } | ||
| 98 | |||
| 99 | 13 | m.isSink = isSink; | |
| 100 | 13 | m.pipeline = pipeline; | |
| 101 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | m.bus = gst_element_get_bus(pipeline); |
| 102 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | m.updatedMS = GetMonotonicTimeMS(); |
| 103 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | gst_element_set_state(pipeline, GST_STATE_PLAYING); |
| 104 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | LogDebug(VB_MEDIAOUT, "AudioLevelMonitor: metering %s\n", node.c_str()); |
| 105 | 13 | return true; | |
| 106 | 13 | } | |
| 107 | |||
| 108 | // Stops and frees a meter taken out of m_meters. Never called with m_lock | ||
| 109 | // held, for the same reason as BuildMeter(). | ||
| 110 | 13 | void AudioLevelMonitor::DestroyMeter(const std::string& node, Meter& m) { | |
| 111 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | if (m.bus) { |
| 112 | 13 | gst_object_unref(m.bus); | |
| 113 | 13 | m.bus = nullptr; | |
| 114 | } | ||
| 115 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | if (m.pipeline) { |
| 116 | 13 | gst_element_set_state(m.pipeline, GST_STATE_NULL); | |
| 117 | 13 | gst_object_unref(m.pipeline); | |
| 118 | 13 | m.pipeline = nullptr; | |
| 119 | } | ||
| 120 | 13 | LogDebug(VB_MEDIAOUT, "AudioLevelMonitor: stopped metering %s\n", node.c_str()); | |
| 121 | 13 | } | |
| 122 | |||
| 123 | 12 | void AudioLevelMonitor::Subscribe(const std::vector<Target>& nodes, int ttlMs) { | |
| 124 | // Bounded: this is reachable from the web UI, and each entry is a real | ||
| 125 | // pipeline against a real audio device. | ||
| 126 | 12 | std::map<std::string, bool> wanted; | |
| 127 |
2/2✓ Branch 0 taken 21 times.
✓ Branch 1 taken 12 times.
|
33 | for (const auto& t : nodes) { |
| 128 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
|
21 | if (t.name.empty()) { |
| 129 | ✗ | continue; | |
| 130 | } | ||
| 131 |
2/8✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 21 times.
|
21 | if (wanted.size() >= 12 && wanted.find(t.name) == wanted.end()) { |
| 132 | ✗ | break; | |
| 133 | } | ||
| 134 |
1/2✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
|
21 | wanted[t.name] = t.isSink; |
| 135 | } | ||
| 136 | |||
| 137 | // Decide under m_lock; build and tear down pipelines outside it (see | ||
| 138 | // BuildMeter()). | ||
| 139 | 12 | std::map<std::string, Meter> dead; | |
| 140 | 12 | std::vector<std::pair<std::string, bool>> toStart; | |
| 141 | { | ||
| 142 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | std::lock_guard<std::mutex> lk(m_lock); |
| 143 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 12 times.
|
21 | for (auto it = m_meters.begin(); it != m_meters.end();) { |
| 144 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | auto w = wanted.find(it->first); |
| 145 | // A node whose kind changed has to be rebuilt: the capture mode is | ||
| 146 | // fixed when the pipeline is created. | ||
| 147 |
5/6✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 8 times.
|
9 | if (w == wanted.end() || w->second != it->second.isSink) { |
| 148 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | dead[it->first] = it->second; |
| 149 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | it = m_meters.erase(it); |
| 150 | } else { | ||
| 151 | 8 | ++it; | |
| 152 | } | ||
| 153 | } | ||
| 154 | // Already-running nodes are left as they are: a keepalive must not | ||
| 155 | // restart a pipeline and blank the meter every few seconds. | ||
| 156 |
2/2✓ Branch 0 taken 21 times.
✓ Branch 1 taken 12 times.
|
33 | for (const auto& kv : wanted) { |
| 157 |
3/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 8 times.
|
21 | if (m_meters.find(kv.first) == m_meters.end()) { |
| 158 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
|
13 | toStart.push_back(kv); |
| 159 | } | ||
| 160 | } | ||
| 161 | 12 | } | |
| 162 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
|
13 | for (auto& kv : dead) { |
| 163 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | DestroyMeter(kv.first, kv.second); |
| 164 | } | ||
| 165 | 12 | dead.clear(); | |
| 166 | |||
| 167 | 12 | std::map<std::string, Meter> built; | |
| 168 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 12 times.
|
25 | for (const auto& n : toStart) { |
| 169 | 13 | Meter m; | |
| 170 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
|
13 | if (BuildMeter(n.first, n.second, m)) { |
| 171 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | built[n.first] = m; |
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | bool needThread; | ||
| 176 | { | ||
| 177 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | std::lock_guard<std::mutex> lk(m_lock); |
| 178 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 12 times.
|
25 | for (auto& kv : built) { |
| 179 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
|
13 | if (m_meters.find(kv.first) != m_meters.end()) { |
| 180 | // Another subscribe started the same node meanwhile. | ||
| 181 | ✗ | dead[kv.first] = kv.second; | |
| 182 | } else { | ||
| 183 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | m_meters[kv.first] = kv.second; |
| 184 | } | ||
| 185 | } | ||
| 186 |
3/4✓ Branch 0 taken 1 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
|
12 | m_expiresMS = m_meters.empty() ? 0 : GetMonotonicTimeMS() + std::max(1000, ttlMs); |
| 187 | 12 | m_active.store(!m_meters.empty()); | |
| 188 | 12 | needThread = !m_meters.empty(); | |
| 189 | 12 | } | |
| 190 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
|
12 | for (auto& kv : dead) { |
| 191 | ✗ | DestroyMeter(kv.first, kv.second); | |
| 192 | } | ||
| 193 | |||
| 194 | // Thread lifecycle is handled outside m_lock, and the previous thread is | ||
| 195 | // joined before a new one is assigned: the poll thread retires itself once | ||
| 196 | // the last meter goes away, and assigning over a still-joinable thread | ||
| 197 | // calls std::terminate() -- which aborted fppd the first time metering was | ||
| 198 | // switched on, off, and on again. | ||
| 199 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 1 times.
|
12 | if (needThread) { |
| 200 |
1/2✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
|
11 | std::lock_guard<std::mutex> tlk(m_threadLock); |
| 201 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 4 times.
|
11 | if (!m_running.load()) { |
| 202 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 6 times.
|
7 | if (m_thread.joinable()) { |
| 203 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | m_thread.join(); |
| 204 | } | ||
| 205 | 7 | m_running.store(true); | |
| 206 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | m_thread = std::thread(&AudioLevelMonitor::PollThread, this); |
| 207 | } | ||
| 208 | 11 | } | |
| 209 | 12 | } | |
| 210 | |||
| 211 | // Drains each pipeline's bus for level messages. A thread rather than bus | ||
| 212 | // callbacks so this owns no GLib main loop -- fppd has no shared one to attach | ||
| 213 | // to, and adding a second is a bigger commitment than a 20ms poll. | ||
| 214 | 7 | void AudioLevelMonitor::PollThread() { | |
| 215 |
2/2✓ Branch 0 taken 977 times.
✓ Branch 1 taken 7 times.
|
984 | while (m_running.load()) { |
| 216 | 977 | std::map<std::string, Meter> expired; | |
| 217 | { | ||
| 218 |
1/2✓ Branch 0 taken 977 times.
✗ Branch 1 not taken.
|
977 | std::lock_guard<std::mutex> lk(m_lock); |
| 219 | |||
| 220 | // Nobody has re-subscribed: tear everything down. This is what | ||
| 221 | // keeps a closed browser tab from metering forever. The | ||
| 222 | // pipelines are stopped below, once m_lock is released. | ||
| 223 |
7/8✓ Branch 0 taken 976 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 976 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 973 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 974 times.
|
977 | if (!m_meters.empty() && GetMonotonicTimeMS() > m_expiresMS) { |
| 224 | 3 | expired.swap(m_meters); | |
| 225 | 3 | m_active.store(false); | |
| 226 | } | ||
| 227 | |||
| 228 |
2/2✓ Branch 0 taken 1752 times.
✓ Branch 1 taken 977 times.
|
2729 | for (auto& kv : m_meters) { |
| 229 | 1752 | GstMessage* msg = nullptr; | |
| 230 |
1/2✓ Branch 0 taken 1948 times.
✗ Branch 1 not taken.
|
1948 | while ((msg = gst_bus_pop_filtered(kv.second.bus, |
| 231 |
2/2✓ Branch 0 taken 196 times.
✓ Branch 1 taken 1752 times.
|
1948 | (GstMessageType)(GST_MESSAGE_ELEMENT | GST_MESSAGE_ERROR))) != nullptr) { |
| 232 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 196 times.
|
196 | if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR) { |
| 233 | ✗ | GError* err = nullptr; | |
| 234 | ✗ | gst_message_parse_error(msg, &err, nullptr); | |
| 235 | ✗ | LogWarn(VB_MEDIAOUT, "AudioLevelMonitor: %s: %s\n", kv.first.c_str(), | |
| 236 | err ? err->message : "pipeline error"); | ||
| 237 | ✗ | if (err) { | |
| 238 | ✗ | g_error_free(err); | |
| 239 | } | ||
| 240 | ✗ | gst_message_unref(msg); | |
| 241 | ✗ | continue; | |
| 242 | ✗ | } | |
| 243 | |||
| 244 |
1/2✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
|
196 | const GstStructure* st = gst_message_get_structure(msg); |
| 245 |
4/8✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 196 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 196 times.
✗ Branch 7 not taken.
|
196 | if (st && gst_structure_has_name(st, "level")) { |
| 246 | // The level element historically reported per-channel | ||
| 247 | // values as GValueArray (GLib) and newer GStreamer as | ||
| 248 | // GstValueArray. Modern GLib deprecates GValueArray in | ||
| 249 | // favour of GArray (G_TYPE_ARRAY), so check GArray first, | ||
| 250 | // then GstValueArray, then fall back to GValueArray via | ||
| 251 | // runtime lookup (avoids G_TYPE_VALUE_ARRAY deprecated | ||
| 252 | // macro which triggers "Deprecated pre-processor symbol" | ||
| 253 | // via _GLIB_GNUC_DO_PRAGMA(GCC warning) not suppressible | ||
| 254 | // by -Wdeprecated-declarations). | ||
| 255 |
1/2✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
|
196 | const GValue* arr = gst_structure_get_value(st, "rms"); |
| 256 | 196 | const GValue* v = nullptr; | |
| 257 |
7/14✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 196 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 196 times.
✓ Branch 8 taken 196 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 196 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 196 times.
|
196 | if (arr && G_VALUE_HOLDS(arr, G_TYPE_ARRAY)) { |
| 258 | ✗ | GArray* va = (GArray*)g_value_get_boxed(arr); | |
| 259 | ✗ | if (va && va->len > 0) { | |
| 260 | ✗ | v = &g_array_index(va, GValue, 0); | |
| 261 | } | ||
| 262 |
4/12✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 196 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 196 times.
|
196 | } else if (arr && GST_VALUE_HOLDS_ARRAY(arr) && gst_value_array_get_size(arr) > 0) { |
| 263 | ✗ | v = gst_value_array_get_value(arr, 0); | |
| 264 |
1/2✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
|
196 | } else if (arr) { |
| 265 | static GType gvaType = 0; | ||
| 266 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 190 times.
|
196 | if (gvaType == 0) { |
| 267 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | gvaType = g_type_from_name("GValueArray"); |
| 268 | } | ||
| 269 |
5/12✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 196 times.
✓ Branch 4 taken 196 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 196 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 196 times.
✗ Branch 11 not taken.
|
196 | if (gvaType != 0 && G_VALUE_HOLDS(arr, gvaType)) { |
| 270 |
1/2✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
|
196 | GValueArray* va = (GValueArray*)g_value_get_boxed(arr); |
| 271 |
2/4✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
|
196 | if (va && va->n_values > 0) { |
| 272 | 196 | v = &va->values[0]; | |
| 273 | } | ||
| 274 | } | ||
| 275 | } | ||
| 276 |
5/12✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 196 times.
✓ Branch 4 taken 196 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 196 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 196 times.
✗ Branch 11 not taken.
|
196 | if (v && G_VALUE_HOLDS_DOUBLE(v)) { |
| 277 |
1/2✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
|
196 | kv.second.rmsDb = g_value_get_double(v); |
| 278 |
1/2✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
|
196 | kv.second.updatedMS = GetMonotonicTimeMS(); |
| 279 | } | ||
| 280 | } | ||
| 281 |
1/2✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
|
196 | gst_message_unref(msg); |
| 282 | } | ||
| 283 | } | ||
| 284 | |||
| 285 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 973 times.
|
977 | if (m_meters.empty()) { |
| 286 | 4 | m_running.store(false); | |
| 287 | } | ||
| 288 | 977 | } | |
| 289 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 977 times.
|
982 | for (auto& kv : expired) { |
| 290 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | DestroyMeter(kv.first, kv.second); |
| 291 | } | ||
| 292 |
1/2✓ Branch 0 taken 977 times.
✗ Branch 1 not taken.
|
977 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); |
| 293 | 977 | } | |
| 294 | 7 | } | |
| 295 | |||
| 296 | 166 | Json::Value AudioLevelMonitor::GetLevels() { | |
| 297 |
1/2✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
|
166 | Json::Value out(Json::objectValue); |
| 298 |
1/2✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
|
166 | std::lock_guard<std::mutex> lk(m_lock); |
| 299 |
1/2✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
|
166 | long long now = GetMonotonicTimeMS(); |
| 300 |
2/2✓ Branch 0 taken 290 times.
✓ Branch 1 taken 166 times.
|
456 | for (const auto& kv : m_meters) { |
| 301 | // A pipeline that has stopped reporting (device gone, node removed) | ||
| 302 | // reads as silent rather than freezing at its last value. | ||
| 303 |
2/2✓ Branch 0 taken 197 times.
✓ Branch 1 taken 93 times.
|
290 | double db = (now - kv.second.updatedMS) > 1000 ? METER_FLOOR_DB : kv.second.rmsDb; |
| 304 | 290 | double pct = ((db - METER_FLOOR_DB) / -METER_FLOOR_DB) * 100.0; | |
| 305 |
3/6✓ Branch 0 taken 290 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 290 times.
✗ Branch 5 not taken.
|
290 | out[kv.first] = (int)std::lround(std::clamp(pct, 0.0, 100.0)); |
| 306 | } | ||
| 307 | 166 | return out; | |
| 308 | 166 | } | |
| 309 | |||
| 310 | 2325 | void AudioLevelMonitor::Shutdown() { | |
| 311 | 2325 | m_running.store(false); | |
| 312 | { | ||
| 313 |
1/2✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
|
2325 | std::lock_guard<std::mutex> tlk(m_threadLock); |
| 314 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2319 times.
|
2325 | if (m_thread.joinable()) { |
| 315 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | m_thread.join(); |
| 316 | } | ||
| 317 | 2325 | } | |
| 318 | 2325 | std::map<std::string, Meter> all; | |
| 319 | { | ||
| 320 |
1/2✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
|
2325 | std::lock_guard<std::mutex> lk(m_lock); |
| 321 | 2325 | all.swap(m_meters); | |
| 322 | 2325 | m_active.store(false); | |
| 323 | 2325 | } | |
| 324 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2325 times.
|
2332 | for (auto& kv : all) { |
| 325 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | DestroyMeter(kv.first, kv.second); |
| 326 | } | ||
| 327 | 2325 | } | |
| 328 | |||
| 329 | #endif | ||
| 330 |