| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | /* | ||
| 3 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 4 | * 2013-2026 by the Falcon Player Developers. | ||
| 5 | * | ||
| 6 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 7 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 8 | * file for descriptions of what files are covered by each license. | ||
| 9 | * | ||
| 10 | * This source file is covered under the LGPL v2.1 as described in the | ||
| 11 | * included LICENSE.LGPL file. | ||
| 12 | */ | ||
| 13 | |||
| 14 | // AudioLevelMonitor -- live signal levels for PipeWire nodes. | ||
| 15 | // | ||
| 16 | // Feeds the mixer's meters. One GStreamer pipeline per metered node, | ||
| 17 | // | ||
| 18 | // pipewiresrc target-object=<node>.monitor ! audioconvert | ||
| 19 | // ! audio/x-raw,channels=1,rate=8000 ! level ! fakesink | ||
| 20 | // | ||
| 21 | // which is how a meter is supposed to be done: the pipeline stays open and the | ||
| 22 | // level element reports RMS on a timer. Measured on a Pi 4 that is ~3% of one | ||
| 23 | // core per node. The naive alternative -- sampling a node by starting a short | ||
| 24 | // capture each refresh -- pays the stream setup cost over and over, and cannot | ||
| 25 | // produce a usable reading anyway, since a burst short enough to be cheap is | ||
| 26 | // dominated by the ~250ms the capture takes to start. | ||
| 27 | // | ||
| 28 | // Metering is entirely on demand. Callers subscribe with a TTL and re-subscribe | ||
| 29 | // to keep it alive, so a browser that closes its tab (or crashes) stops costing | ||
| 30 | // anything within a few seconds without needing to tell us. | ||
| 31 | |||
| 32 | #if __has_include(<gst/gst.h>) | ||
| 33 | #define HAS_AUDIO_LEVEL_MONITOR | ||
| 34 | |||
| 35 | #include <gst/gst.h> | ||
| 36 | |||
| 37 | #include <atomic> | ||
| 38 | #include <map> | ||
| 39 | #include <mutex> | ||
| 40 | #include <set> | ||
| 41 | #include <string> | ||
| 42 | #include <thread> | ||
| 43 | #include <vector> | ||
| 44 | |||
| 45 | #include "fpp-json.h" | ||
| 46 | |||
| 47 | class AudioLevelMonitor { | ||
| 48 | public: | ||
| 49 | static AudioLevelMonitor INSTANCE; | ||
| 50 | |||
| 51 | // A node to meter. isSink distinguishes a sink (a group, a member's filter | ||
| 52 | // chain, an input bus) from a playback stream (fppd_stream_N): capturing a | ||
| 53 | // sink needs stream.capture.sink, and setting it on a stream instead makes | ||
| 54 | // pipewiresrc fall back to the default sink -- see BuildMeter(). | ||
| 55 | struct Target { | ||
| 56 | std::string name; | ||
| 57 | bool isSink = true; | ||
| 58 | }; | ||
| 59 | |||
| 60 | // Meter exactly these nodes for the next ttlMs. Pipelines for nodes no | ||
| 61 | // longer listed are torn down; nodes already running are left alone so a | ||
| 62 | // keepalive does not interrupt them. | ||
| 63 | void Subscribe(const std::vector<Target>& nodes, int ttlMs); | ||
| 64 | |||
| 65 | // { "<node>": <0-100>, ... } -- empty when nothing is subscribed, which is | ||
| 66 | // what makes this free on an idle system. | ||
| 67 | Json::Value GetLevels(); | ||
| 68 | |||
| 69 | // True while any pipeline is running, so the WebSocket only pays for a | ||
| 70 | // faster broadcast while someone is actually watching meters. | ||
| 71 | 959 | bool Active() const { return m_active.load(); } | |
| 72 | |||
| 73 | void Shutdown(); | ||
| 74 | |||
| 75 | private: | ||
| 76 | 2346 | AudioLevelMonitor() = default; | |
| 77 | ~AudioLevelMonitor(); | ||
| 78 | |||
| 79 | struct Meter { | ||
| 80 | GstElement* pipeline = nullptr; | ||
| 81 | GstBus* bus = nullptr; | ||
| 82 | // Written by the bus poll, read by GetLevels(). | ||
| 83 | double rmsDb = -100.0; | ||
| 84 | long long updatedMS = 0; | ||
| 85 | bool isSink = true; | ||
| 86 | }; | ||
| 87 | |||
| 88 | bool BuildMeter(const std::string& node, bool isSink, Meter& m); | ||
| 89 | void DestroyMeter(const std::string& node, Meter& m); | ||
| 90 | void PollThread(); | ||
| 91 | |||
| 92 | mutable std::mutex m_lock; | ||
| 93 | // Guards thread lifecycle only. Deliberately not m_lock: joining the poll | ||
| 94 | // thread while holding m_lock would deadlock, because the thread takes | ||
| 95 | // m_lock on every pass. | ||
| 96 | std::mutex m_threadLock; | ||
| 97 | std::map<std::string, Meter> m_meters; | ||
| 98 | long long m_expiresMS = 0; | ||
| 99 | std::atomic<bool> m_active{ false }; | ||
| 100 | std::atomic<bool> m_running{ false }; | ||
| 101 | std::thread m_thread; | ||
| 102 | }; | ||
| 103 | |||
| 104 | #endif | ||
| 105 |