GCC Code Coverage Report


Directory: ./
File: mediaoutput/PipeWireOutputStream.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 659 723 91.1%
Functions: 49 54 90.7%
Branches: 594 1026 57.9%

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 "PipeWireOutputStream.h"
16
17 #ifdef HAS_GSTREAMER
18
19 #include <gst/app/gstappsink.h>
20 #include <gst/app/gstappsrc.h>
21
22 #if defined(__ARM_NEON) || defined(__ARM_NEON__)
23 #include <arm_neon.h>
24 #endif
25
26 #include <algorithm>
27 #include <array>
28 #include <atomic>
29 #include <chrono>
30 #include <cmath>
31 #include <ctime>
32 #include <cstdio>
33 #include <cstring>
34 #include <thread>
35 #include <vector>
36
37 #include "common.h"
38 #include "common_mini.h"
39 #include "log.h"
40 #include "settings.h"
41 #include "StreamSlotManager.h"
42
43 namespace {
44 // Every stream carries F32 stereo at the graph's own rate, so PipeWire never
45 // resamples it and a feed only converts what differs.
46 constexpr int kChannels = 2;
47 constexpr int kBytesPerFrame = 4 * kChannels;
48
49 // Time from a buffer handed to the stream to it being written to the card,
50 // on top of the card's own queue: pipewiresink queues it for the graph's next
51 // cycle, so one graph quantum (FPP's default is 1024 frames).
52 constexpr int64_t kGraphQuantum = 1024;
53
54 // Longest a stream may take to come up before it is abandoned. A cold open is
55 // ~0.6 s on an AM335x; this only trips when PipeWire itself is unwell.
56 constexpr int kOpenTimeoutMs = 8000;
57 // How often an opening stream's links are checked (each check runs pw-link).
58 constexpr int kLinkCheckIntervalMs = 50;
59 // With no card to watch (a group of network sinks), how long the stream must
60 // have been linked before it is trusted to be on the real driver.
61 constexpr int kNoCardSettleMs = 150;
62 // With the stream kept open, how long after a failure to try again. A
63 // PipeWire restart under a running fppd leaves GStreamer's PipeWire connection
64 // dead until fppd restarts, so retrying hard would only fill the log.
65 constexpr int kKeepOpenRetryMs = 30000;
66 // Silence fed after a flush between tracks, which the reactivated stream
67 // drops in place of the next track's head (see Flush()). Measured on a Pi 5
68 // I2S HAT: with none, 30-60 ms of the head went; with 60 ms, none did.
69 constexpr int kFlushPrimeMs = 60;
70
71 // The card clock (see DisciplineClock()). How often the card's position is
72 // read, how much of its history the rate is fitted over, how fast a phase
73 // error is closed, and how far the clock may be pushed off the system's.
74 constexpr int kClockSampleMs = 250;
75 constexpr double kClockWindowS = 120.0;
76 constexpr double kClockMinFitS = 4.0;
77 constexpr double kClockPhaseGain = 1.0 / 20.0; // per second of error, per second
78 constexpr double kClockMaxPhaseSlew = 500e-6;
79 constexpr double kClockMaxOffset = 2000e-6;
80 // A jump this big in the card's position is a restart or an xrun, not drift:
81 // start again from it rather than chase it.
82 constexpr double kClockMaxPhaseError = 0.050;
83 // How long after one of our own flushes the card stopping or restarting is
84 // put down to the flush. A flush takes the stream inactive and back, and
85 // where it is the card's only follower (a Pi 4's headphone jack) PipeWire
86 // stops the device and starts it again; the samples a few hundred ms later
87 // see that.
88 constexpr uint64_t kFlushRestartMs = 1000;
89
90 std::mutex s_slotsLock;
91 std::array<PipeWireOutputStream*, StreamSlotManager::MAX_SLOTS + 1> s_slots{};
92
93 std::mutex s_workerLock;
94 std::condition_variable s_workerWake;
95 std::thread s_worker;
96 bool s_workerRunning = false;
97 // Set by ShutdownAll() and never cleared: a ForSlot() racing it (a prewarm, a
98 // settings listener) must not start a worker again, let alone assign a new
99 // std::thread over the one ShutdownAll() is joining.
100 bool s_workerShutDown = false;
101 bool s_workerKick = false;
102
103 std::atomic<bool> s_keepOpen{ false };
104
105 37424 uint64_t NowMs() {
106 37424 return (uint64_t)GetMonotonicTimeMS(); // holds and timeouts: must not step with the wall clock
107 }
108 }
109
110 163 PipeWireOutputStream::PipeWireOutputStream(int slot) :
111
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 m_slot(slot) {
112 163 }
113
114 5931 PipeWireOutputStream* PipeWireOutputStream::ForSlot(int slot) {
115
7/10
✓ Branch 0 taken 5931 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5931 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5931 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 5929 times.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 5929 times.
5931 if (slot < 1 || slot > StreamSlotManager::MAX_SLOTS || !isPipeWireBackend()) {
116 2 return nullptr;
117 }
118
1/2
✓ Branch 0 taken 5929 times.
✗ Branch 1 not taken.
5929 std::unique_lock<std::mutex> lock(s_slotsLock);
119
2/2
✓ Branch 0 taken 163 times.
✓ Branch 1 taken 5766 times.
5929 if (!s_slots[slot]) {
120
3/8
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 163 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
163 s_slots[slot] = new PipeWireOutputStream(slot);
121 }
122 {
123
1/2
✓ Branch 0 taken 5929 times.
✗ Branch 1 not taken.
5929 std::unique_lock<std::mutex> wlock(s_workerLock);
124
4/4
✓ Branch 0 taken 300 times.
✓ Branch 1 taken 5629 times.
✓ Branch 2 taken 150 times.
✓ Branch 3 taken 150 times.
5929 if (!s_workerRunning && !s_workerShutDown) {
125 150 s_workerRunning = true;
126
1/2
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
150 s_worker = std::thread(WorkerLoop);
127 }
128 5929 }
129 5929 return s_slots[slot];
130 5929 }
131
132 2172 void PipeWireOutputStream::Prewarm(int slot, int holdMs) {
133
1/2
✓ Branch 0 taken 2172 times.
✗ Branch 1 not taken.
2172 PipeWireOutputStream* s = ForSlot(slot);
134
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2170 times.
2172 if (!s) {
135 2 return;
136 }
137
1/2
✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
2170 std::string target = GStreamerOutput::PipeWireSinkNameForSlot(slot);
138 {
139
1/2
✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
2170 std::unique_lock<std::mutex> lock(s->m_lock);
140
1/2
✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
2170 s->m_wantTarget = target;
141
1/2
✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
2170 s->m_holdUntilMs = std::max(s->m_holdUntilMs, NowMs() + (uint64_t)std::max(holdMs, 0));
142 2170 s->m_wantOpen = true;
143
1/2
✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
2170 s->ForgetFailureLocked();
144 2170 }
145
1/2
✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
2170 WakeWorker();
146 2170 }
147
148 1887 void PipeWireOutputStream::SetKeepOpen(bool keepOpen) {
149
2/2
✓ Branch 0 taken 1870 times.
✓ Branch 1 taken 17 times.
1887 if (s_keepOpen.exchange(keepOpen) == keepOpen) {
150 1870 return;
151 }
152
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 if (keepOpen) {
153 17 LogInfo(VB_MEDIAOUT, "PipeWire output stream 1 will be kept open\n");
154 17 Prewarm(1, 0);
155 } else {
156 // Falls back to the usual linger; the worker closes it once that
157 // and any playing track are done.
158 ✗ LogInfo(VB_MEDIAOUT, "PipeWire output stream 1 no longer kept open\n");
159 ✗ if (PipeWireOutputStream* s = ForSlot(1)) {
160 ✗ std::unique_lock<std::mutex> lock(s->m_lock);
161 ✗ s->m_holdUntilMs = std::max(s->m_holdUntilMs, NowMs() + LINGER_MS);
162 ✗ }
163 ✗ WakeWorker();
164 }
165 }
166
167 2085 std::string PipeWireOutputStream::CapsFields() {
168
2/4
✓ Branch 0 taken 2085 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2085 times.
✗ Branch 3 not taken.
4170 return ",format=F32LE,rate=" + std::to_string(GStreamerOutput::PipeWireGraphRate()) +
169
3/6
✓ Branch 0 taken 2085 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2085 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2085 times.
✗ Branch 5 not taken.
6191 ",channels=" + std::to_string(kChannels) + ",layout=interleaved";
170 }
171
172 1867 void PipeWireOutputStream::ShutdownAll() {
173 1867 std::thread worker;
174 {
175
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::unique_lock<std::mutex> wlock(s_workerLock);
176 1867 s_workerShutDown = true;
177
2/2
✓ Branch 0 taken 1717 times.
✓ Branch 1 taken 150 times.
1867 if (!s_workerRunning) {
178 1717 return;
179 }
180 150 s_workerRunning = false;
181 150 worker = std::move(s_worker);
182
2/2
✓ Branch 0 taken 150 times.
✓ Branch 1 taken 1717 times.
1867 }
183 150 s_workerWake.notify_all();
184
1/2
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
150 if (worker.joinable()) {
185
1/2
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
150 worker.join();
186 }
187
1/2
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
150 std::unique_lock<std::mutex> lock(s_slotsLock);
188
2/2
✓ Branch 0 taken 900 times.
✓ Branch 1 taken 150 times.
1050 for (PipeWireOutputStream* s : s_slots) {
189
2/2
✓ Branch 0 taken 163 times.
✓ Branch 1 taken 737 times.
900 if (s) {
190
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 std::unique_lock<std::mutex> slock(s->m_lock);
191 163 s->m_users = 0;
192 163 s->m_holdUntilMs = 0;
193
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 s->CloseLocked("fppd shutting down");
194 163 }
195 }
196
2/2
✓ Branch 0 taken 150 times.
✓ Branch 1 taken 1717 times.
1867 }
197
198 1881 void PipeWireOutputStream::Acquire(const std::string& target) {
199 {
200
1/2
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
1881 std::unique_lock<std::mutex> lock(m_lock);
201 1881 m_users++;
202
1/2
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
1881 m_wantTarget = target;
203 1881 m_wantOpen = true;
204
1/2
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
1881 ForgetFailureLocked();
205 1881 }
206 1881 WakeWorker();
207 1881 }
208
209 // A failure belongs to the attempt that made it. Left in place, the next
210 // track's WaitReady() returned on it at once while the worker was already
211 // opening the stream again, and the head of that track went nowhere.
212 4051 void PipeWireOutputStream::ForgetFailureLocked() {
213
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 4018 times.
4051 if (m_state == State::Failed) {
214 33 m_state = State::Closed;
215 }
216 4051 }
217
218 1836 void PipeWireOutputStream::Release(GstElement* feed) {
219 {
220
1/2
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
1836 std::unique_lock<std::mutex> lock(m_lock);
221
2/4
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1836 times.
✗ Branch 3 not taken.
1836 if (feed && m_feed == feed) {
222 1836 m_feed = nullptr;
223 // Push the track's last fragment out behind some silence. PipeWire
224 // holds a partial cycle until there is more data, so with nothing
225 // behind it the tail of a stopped track sat in the stream and played
226 // at the head of the next one. (Flushing here instead would drop
227 // the tail; WaitDrained() flushes only once it has played.)
228
2/4
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1836 times.
✗ Branch 3 not taken.
1836 PushSilenceLocked(TailFlushMs());
229 }
230
1/2
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
1836 if (m_users > 0) {
231 1836 m_users--;
232 }
233
1/2
✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
1836 m_holdUntilMs = std::max(m_holdUntilMs, NowMs() + LINGER_MS);
234 1836 }
235 1836 WakeWorker();
236 1836 }
237
238 415073 static int64_t MonoUs() {
239 struct timespec ts;
240 415073 clock_gettime(CLOCK_MONOTONIC, &ts);
241 415073 return (int64_t)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000;
242 }
243
244 55628 void PipeWireOutputStream::NoteQueuedLocked(size_t bytes) {
245
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 55628 times.
55628 if (m_rate <= 0) {
246 ✗ return;
247 }
248 55628 int64_t now = MonoUs();
249 55628 int64_t durUs = (int64_t)(bytes / kBytesPerFrame) * 1000000LL / m_rate;
250 55628 m_queuedUntilUs = std::max(m_queuedUntilUs, now) + durUs;
251 }
252
253 // Whether the queue drained before the deadline.
254 178256 bool PipeWireOutputStream::WaitQueuedUntil(int64_t deadlineUs) {
255 while (true) {
256 int64_t remaining;
257 {
258
1/2
✓ Branch 0 taken 178256 times.
✗ Branch 1 not taken.
178256 std::unique_lock<std::mutex> lock(m_lock);
259 178256 remaining = m_queuedUntilUs - MonoUs();
260 178256 }
261
2/2
✓ Branch 0 taken 815 times.
✓ Branch 1 taken 177441 times.
178256 if (remaining <= 0) {
262 3748 return true;
263 }
264
2/2
✓ Branch 0 taken 2933 times.
✓ Branch 1 taken 174508 times.
177441 if (MonoUs() >= deadlineUs) {
265 2933 return false;
266 }
267
1/2
✓ Branch 0 taken 174508 times.
✗ Branch 1 not taken.
174508 std::this_thread::sleep_for(std::chrono::microseconds(std::min<int64_t>(remaining, 5000)));
268 174508 }
269 }
270
271 1874 void PipeWireOutputStream::WaitDrained(int maxMs) {
272 1874 int64_t deadline = MonoUs() + (int64_t)maxMs * 1000;
273 1874 WaitQueuedUntil(deadline);
274 1874 Flush(true);
275 // The silence Flush() pushed must play before the track goes to PLAYING,
276 // or the track starts that much behind its position.
277 1874 WaitQueuedUntil(MonoUs() + (int64_t)(kFlushPrimeMs + 20) * 1000);
278 1874 }
279
280 // m_queuedUntilUs only knows what was handed to the stream and assumes the
281 // graph takes it at real time. When the graph stalls instead -- an overloaded
282 // board, an xrun -- the card falls behind, the feed doesn't (it runs on the
283 // clock), and the difference stays queued inside pipewiresink, where nothing
284 // here can see it. With the stream held open and fed track after track it
285 // never drains: the sound stays that far behind the lights until fppd
286 // restarts. One 300 ms stall on a Pi 5 left every later song 280 ms behind
287 // (issue #3083).
288 //
289 // So between tracks, once the estimate says the last one has played, flush:
290 // pipewiresink drops whatever its stream still holds (pw_stream_flush()),
291 // which is nothing unless there was a stall, and every track starts from an
292 // empty queue. The first track on a newly opened stream is flushed as well.
293 // Left as it opened, the stream's queue settled a whole graph cycle deeper on
294 // some opens than others, and the sound landed anywhere from 1 to 28 ms off
295 // the lights from one show start to the next on a Pi 5; flushed, 12-20 ms.
296 // The stream goes inactive and back, and drops the first few tens of ms
297 // handed to it after that, so between tracks it is primed with silence.
298 //
299 // When the stall is an xrun on the card itself, DisciplineClock() sees the
300 // card stop or restart, and the stream is flushed right then, mid-track,
301 // unprimed: the sound skips what the stall held up -- the xrun was already
302 // audible -- and is back with the lights at once rather than at the next
303 // track. A stall that never reaches the card (a late group node) is left to
304 // the flush between tracks.
305 1896 void PipeWireOutputStream::Flush(bool prime) {
306 1896 GstElement* src = nullptr;
307 {
308
1/2
✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
1896 std::unique_lock<std::mutex> lock(m_lock);
309
6/6
✓ Branch 0 taken 1886 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 1885 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1544 times.
✓ Branch 5 taken 341 times.
1896 if (!m_src || m_state != State::Ready || m_flushed) {
310 1555 return;
311 }
312 341 m_flushed = true;
313
1/2
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
341 m_lastFlushMs = NowMs();
314
3/6
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 341 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 341 times.
✗ Branch 5 not taken.
341 src = GST_ELEMENT(gst_object_ref(m_src));
315
2/2
✓ Branch 0 taken 341 times.
✓ Branch 1 taken 1555 times.
1896 }
316 // Outside the lock: the flush takes the sink's own locks.
317 341 gst_element_send_event(src, gst_event_new_flush_start());
318 341 gst_element_send_event(src, gst_event_new_flush_stop(FALSE));
319 {
320
1/2
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
341 std::unique_lock<std::mutex> lock(m_lock);
321
1/2
✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
341 if (m_src == src) {
322 341 m_queuedUntilUs = 0;
323
2/2
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 22 times.
341 if (prime) {
324
1/2
✓ Branch 0 taken 319 times.
✗ Branch 1 not taken.
319 PushSilenceLocked(kFlushPrimeMs);
325 }
326 }
327 341 }
328 341 gst_object_unref(src);
329
2/2
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 22 times.
341 LogDebug(VB_MEDIAOUT, "PipeWire output stream %d flushed%s\n", m_slot, prime ? " between tracks" : "");
330 }
331
332 1881 bool PipeWireOutputStream::WaitReady(int timeoutMs) {
333 // Ready only counts on the target asked for: a stream still Ready on the
334 // previous sink is about to be closed and reopened by the worker.
335
1/2
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
1881 std::unique_lock<std::mutex> lock(m_lock);
336
4/4
✓ Branch 0 taken 3729 times.
✓ Branch 1 taken 164 times.
✓ Branch 2 taken 3728 times.
✓ Branch 3 taken 1 times.
3893 auto readyHere = [this] { return m_state == State::Ready && m_target == m_wantTarget; };
337
1/2
✓ Branch 0 taken 1874 times.
✗ Branch 1 not taken.
1881 m_stateChanged.wait_for(lock, std::chrono::milliseconds(timeoutMs), [this, &readyHere] {
338
4/4
✓ Branch 0 taken 155 times.
✓ Branch 1 taken 1864 times.
✓ Branch 2 taken 146 times.
✓ Branch 3 taken 9 times.
2165 return readyHere() || m_state == State::Failed ||
339
3/4
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 98 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 48 times.
2165 (m_state == State::Closed && !m_wantOpen);
340 });
341 3748 return readyHere();
342 1874 }
343
344 1836 int PipeWireOutputStream::TailFlushMs() {
345 // Three graph cycles: more than any partial cycle plus the feed's lead.
346 1836 return (int)(3 * kGraphQuantum * 1000 / GStreamerOutput::PipeWireGraphRate()) + 1;
347 }
348
349 1881 int64_t PipeWireOutputStream::FeedLeadNs() {
350 1881 return kGraphQuantum * 1000000000LL / GStreamerOutput::PipeWireGraphRate();
351 }
352
353 1881 void PipeWireOutputStream::AttachFeed(GstElement* appsink) {
354 {
355
1/2
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
1881 std::unique_lock<std::mutex> lock(m_lock);
356 1881 m_feed = appsink;
357 // Each feed starts at the card's own pace. The feed clock is put back
358 // on the card clock now, before this feed's pipeline has a base time
359 // on it, so a speed the last track was steered at doesn't carry over.
360 1881 m_feedSpeed = 1.0;
361 1881 m_feedDiverged = false;
362 1881 m_rsActive = false;
363 1881 m_rsPos = 0.0;
364 1881 m_rsHistFrames = 0;
365
2/4
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1881 times.
✗ Branch 3 not taken.
1881 if (m_clock && m_feedClock) {
366
2/4
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1881 times.
✗ Branch 3 not taken.
1881 RecalibrateFeedClockLocked(gst_clock_get_internal_time(m_clock));
367 }
368 1881 }
369 // Run the feed one graph cycle ahead of the clock. Handed over exactly
370 // on time, a buffer can miss the cycle that needed it by a hair; PipeWire
371 // then plays a quantum of silence there and the rest of the track is a
372 // quantum late. A cycle's worth queued absorbs that.
373 1881 g_object_set(appsink, "emit-signals", TRUE, "ts-offset", -FeedLeadNs(), NULL);
374 1881 g_signal_connect(appsink, "new-sample", G_CALLBACK(OnFeedSample), this);
375 1881 }
376
377 // Runs on the feeding pipeline's streaming thread. `this` is a per-slot
378 // singleton that is never freed, so a feed outliving its GStreamerOutput (async
379 // teardown) can still land here safely; it is simply no longer m_feed.
380 54023 GstFlowReturn PipeWireOutputStream::OnFeedSample(GstElement* appsink, gpointer userData) {
381 54023 PipeWireOutputStream* self = static_cast<PipeWireOutputStream*>(userData);
382 54023 GstSample* sample = gst_app_sink_pull_sample(GST_APP_SINK(appsink));
383
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 54023 times.
54023 if (!sample) {
384 ✗ return GST_FLOW_OK;
385 }
386 54023 GstBuffer* in = gst_sample_get_buffer(sample);
387 54023 const GstSegment* segment = gst_sample_get_segment(sample);
388 54023 GstCaps* caps = gst_sample_get_caps(sample);
389
390 54023 GstElement* src = nullptr;
391 54023 GstBuffer* out = nullptr;
392 {
393
1/2
✓ Branch 0 taken 54023 times.
✗ Branch 1 not taken.
54023 std::unique_lock<std::mutex> lock(self->m_lock);
394
6/8
✓ Branch 0 taken 54023 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 54023 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 53323 times.
✓ Branch 5 taken 700 times.
✓ Branch 6 taken 53322 times.
✓ Branch 7 taken 1 times.
54023 bool ours = in && caps && self->m_src && self->m_feed == appsink;
395
6/10
✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
✓ Branch 2 taken 53322 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 53322 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 53322 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 54023 times.
54023 if (ours && self->m_caps && !gst_caps_can_intersect(self->m_caps, caps)) {
396 // Can't happen unless the graph rate moved under a running fppd;
397 // playing it would be noise, so drop it and say so once.
398 ✗ if (!self->m_warnedCaps) {
399 ✗ self->m_warnedCaps = true;
400 ✗ gchar* got = gst_caps_to_string(caps);
401 ✗ gchar* want = gst_caps_to_string(self->m_caps);
402 ✗ LogErr(VB_MEDIAOUT, "PipeWire output stream %d: feed is %s, stream is %s -- dropping it\n",
403 self->m_slot, got, want);
404 ✗ g_free(got);
405 ✗ g_free(want);
406 }
407 ✗ ours = false;
408 }
409
2/2
✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
54023 if (ours) {
410 // Steered to another speed (SetFeedSpeed()), the media is
411 // resampled to the card's rate here; otherwise it goes in as it
412 // came, and only its last frames are kept for a later start.
413
2/2
✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 40888 times.
53322 if (self->m_rsActive) {
414
1/2
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
12434 out = self->ResampleLocked(in);
415 } else {
416
1/2
✓ Branch 0 taken 40888 times.
✗ Branch 1 not taken.
40888 self->KeepResampleHistoryLocked(in);
417
1/2
✓ Branch 0 taken 40888 times.
✗ Branch 1 not taken.
40888 out = gst_buffer_copy(in);
418 }
419 53322 ours = out != nullptr;
420 }
421
2/2
✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
54023 if (ours) {
422 // The feed clock is the card clock (until it is steered, then off
423 // it by however far it has run ahead or behind), so a buffer's
424 // running time in the feeding pipeline maps onto ours through the
425 // two base times and that offset. The sink doesn't sync on it
426 // (the feed already paced the buffer); it is carried for
427 // PipeWire's own bookkeeping.
428
1/2
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
53322 GstClockTime rt = gst_segment_to_running_time(segment, GST_FORMAT_TIME, GST_BUFFER_PTS(in));
429
1/2
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
53322 if (GST_CLOCK_TIME_IS_VALID(rt)) {
430
1/2
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
53322 GstClockTime abs = gst_element_get_base_time(appsink) + rt;
431
2/2
✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 40888 times.
53322 if (self->m_feedDiverged) {
432
2/4
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12434 times.
✗ Branch 3 not taken.
12434 GstClockTimeDiff ahead = GST_CLOCK_DIFF(gst_clock_get_time(self->m_clock),
433 gst_clock_get_time(self->m_feedClock));
434
3/4
✓ Branch 0 taken 877 times.
✓ Branch 1 taken 11557 times.
✓ Branch 2 taken 877 times.
✗ Branch 3 not taken.
12434 abs = ahead > 0 && (GstClockTime)ahead > abs ? 0 : abs - ahead;
435 }
436
1/2
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
53322 GstClockTime ourBase = gst_element_get_base_time(self->m_pipeline);
437
1/2
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
53322 GST_BUFFER_PTS(out) = abs > ourBase ? abs - ourBase : 0;
438 } else {
439 ✗ GST_BUFFER_PTS(out) = GST_CLOCK_TIME_NONE;
440 }
441
2/2
✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 40888 times.
53322 if (self->m_rsActive) {
442 12434 GST_BUFFER_DURATION(out) = self->m_rate > 0
443
3/6
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12434 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12434 times.
✗ Branch 5 not taken.
12434 ? gst_util_uint64_scale(gst_buffer_get_size(out) / kBytesPerFrame, GST_SECOND, self->m_rate)
444 : GST_CLOCK_TIME_NONE;
445 }
446 53322 GST_BUFFER_DTS(out) = GST_CLOCK_TIME_NONE;
447
2/4
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53322 times.
✗ Branch 3 not taken.
53322 self->NoteQueuedLocked(gst_buffer_get_size(out));
448 53322 self->m_flushed = false;
449
3/6
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53322 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 53322 times.
✗ Branch 5 not taken.
53322 src = GST_ELEMENT(gst_object_ref(self->m_src));
450 }
451 54023 }
452
2/2
✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
54023 if (src) {
453 // Outside the lock: the push can take the sink's own locks.
454 53322 gst_app_src_push_buffer(GST_APP_SRC(src), out);
455 53322 gst_object_unref(src);
456 }
457 54023 gst_sample_unref(sample);
458 54023 return GST_FLOW_OK;
459 }
460
461 5887 void PipeWireOutputStream::WakeWorker() {
462 {
463
1/2
✓ Branch 0 taken 5887 times.
✗ Branch 1 not taken.
5887 std::unique_lock<std::mutex> wlock(s_workerLock);
464 5887 s_workerKick = true;
465 5887 }
466 5887 s_workerWake.notify_all();
467 5887 }
468
469 150 void PipeWireOutputStream::WorkerLoop() {
470
2/4
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150 times.
✗ Branch 3 not taken.
300 SetThreadName("FPP-PWStream");
471 while (true) {
472 // Slots are created on demand but never freed, so a snapshot of the
473 // table is safe to service without holding s_slotsLock -- which
474 // ForSlot() callers on the main loop would otherwise wait on while a
475 // stream connects to the daemon.
476 std::array<PipeWireOutputStream*, StreamSlotManager::MAX_SLOTS + 1> slots;
477 {
478
1/2
✓ Branch 0 taken 19423 times.
✗ Branch 1 not taken.
19423 std::unique_lock<std::mutex> lock(s_slotsLock);
479 19423 slots = s_slots;
480 19423 }
481 19423 bool anyOpening = false;
482
1/2
✓ Branch 0 taken 19423 times.
✗ Branch 1 not taken.
19423 uint64_t now = NowMs();
483
2/2
✓ Branch 0 taken 116538 times.
✓ Branch 1 taken 19423 times.
135961 for (PipeWireOutputStream* s : slots) {
484
2/2
✓ Branch 0 taken 28502 times.
✓ Branch 1 taken 88036 times.
116538 if (s) {
485
3/4
✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 28496 times.
28502 if (s->DisciplineClock()) {
486
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogInfo(VB_MEDIAOUT, "PipeWire output stream %d: the card lost time (xrun) while playing; "
487 "flushing so the sound stays with the lights\n", s->m_slot);
488
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 s->Flush(false);
489 }
490
1/2
✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
28502 s->Service(now);
491
1/2
✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
28502 std::unique_lock<std::mutex> slock(s->m_lock);
492 28502 anyOpening |= (s->m_state == State::Opening);
493 28502 }
494 }
495
1/2
✓ Branch 0 taken 19423 times.
✗ Branch 1 not taken.
19423 std::unique_lock<std::mutex> wlock(s_workerLock);
496
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 19422 times.
19423 if (!s_workerRunning) {
497 1 break;
498 }
499 // Opening streams are polled every 10 ms; otherwise the only thing to
500 // watch for is a linger running out.
501
3/4
✓ Branch 0 taken 2844 times.
✓ Branch 1 taken 16578 times.
✓ Branch 2 taken 19422 times.
✗ Branch 3 not taken.
19422 s_workerWake.wait_for(wlock, std::chrono::milliseconds(anyOpening ? 10 : 250),
502
4/4
✓ Branch 0 taken 33116 times.
✓ Branch 1 taken 5538 times.
✓ Branch 2 taken 148 times.
✓ Branch 3 taken 32968 times.
38654 [] { return s_workerKick || !s_workerRunning; });
503 19422 s_workerKick = false;
504
2/2
✓ Branch 0 taken 149 times.
✓ Branch 1 taken 19273 times.
19422 if (!s_workerRunning) {
505 149 break;
506 }
507
2/2
✓ Branch 0 taken 19273 times.
✓ Branch 1 taken 150 times.
38696 }
508 150 }
509
510 28502 void PipeWireOutputStream::Service(uint64_t now) {
511
1/2
✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
28502 std::unique_lock<std::mutex> lock(m_lock);
512
513
3/4
✓ Branch 0 taken 2041 times.
✓ Branch 1 taken 26461 times.
✓ Branch 2 taken 2041 times.
✗ Branch 3 not taken.
28502 bool keepOpen = s_keepOpen && m_slot == 1;
514
3/8
✓ Branch 0 taken 2041 times.
✓ Branch 1 taken 26461 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2041 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
28502 if (keepOpen && !m_wantOpen && (m_state == State::Closed || m_state == State::Failed) &&
515 ✗ now - m_failedMs >= (uint64_t)kKeepOpenRetryMs) {
516 ✗ if (m_wantTarget.empty()) {
517 ✗ m_wantTarget = GStreamerOutput::PipeWireSinkNameForSlot(m_slot);
518 }
519 ✗ m_wantOpen = true;
520 }
521
6/6
✓ Branch 0 taken 26461 times.
✓ Branch 1 taken 2041 times.
✓ Branch 2 taken 11208 times.
✓ Branch 3 taken 15253 times.
✓ Branch 4 taken 10392 times.
✓ Branch 5 taken 816 times.
28502 bool wanted = keepOpen || m_users > 0 || now < m_holdUntilMs;
522
2/2
✓ Branch 0 taken 816 times.
✓ Branch 1 taken 27686 times.
28502 if (!wanted) {
523 816 m_wantOpen = false;
524
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 799 times.
816 if (m_state != State::Closed) {
525
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 CloseLocked("idle");
526 }
527 816 return;
528 }
529
530
2/2
✓ Branch 0 taken 27143 times.
✓ Branch 1 taken 543 times.
27686 if (m_pipeline) {
531 // A bus error means the stream is dead (PipeWire restarted, sink gone).
532 // Drop it; the next track or prewarm opens a fresh one.
533
1/2
✓ Branch 0 taken 27143 times.
✗ Branch 1 not taken.
27143 GstBus* bus = gst_element_get_bus(m_pipeline);
534
2/4
✓ Branch 0 taken 27143 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 27143 times.
27143 while (GstMessage* msg = gst_bus_pop_filtered(bus, GST_MESSAGE_ERROR)) {
535 ✗ GError* err = nullptr;
536 ✗ gst_message_parse_error(msg, &err, nullptr);
537 ✗ LogWarn(VB_MEDIAOUT, "PipeWire output stream %d: %s\n", m_slot, err ? err->message : "error");
538 ✗ if (err) {
539 ✗ g_error_free(err);
540 }
541 ✗ gst_message_unref(msg);
542 ✗ m_wantOpen = false;
543 ✗ CloseLocked("stream error");
544 ✗ m_state = State::Failed;
545 ✗ m_failedMs = now;
546 ✗ m_stateChanged.notify_all();
547 ✗ }
548
1/2
✓ Branch 0 taken 27143 times.
✗ Branch 1 not taken.
27143 gst_object_unref(bus);
549 }
550
551
8/8
✓ Branch 0 taken 27484 times.
✓ Branch 1 taken 202 times.
✓ Branch 2 taken 27143 times.
✓ Branch 3 taken 341 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 27142 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 27685 times.
27686 if (m_state != State::Closed && m_state != State::Failed && m_wantTarget != m_target) {
552
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogInfo(VB_MEDIAOUT, "PipeWire output stream %d: target changed '%s' -> '%s', reopening\n",
553 m_slot, m_target.c_str(), m_wantTarget.c_str());
554
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 CloseLocked("target changed");
555 1 m_wantOpen = true;
556 }
557
558
6/6
✓ Branch 0 taken 27483 times.
✓ Branch 1 taken 203 times.
✓ Branch 2 taken 341 times.
✓ Branch 3 taken 27142 times.
✓ Branch 4 taken 203 times.
✓ Branch 5 taken 341 times.
27686 if ((m_state == State::Closed || m_state == State::Failed) && m_wantOpen) {
559
3/4
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✓ Branch 3 taken 153 times.
203 if (!OpenLocked(lock)) {
560 50 m_wantOpen = false;
561 50 m_state = State::Failed;
562
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 m_failedMs = NowMs();
563 50 m_stateChanged.notify_all();
564 }
565 203 return;
566 }
567
568
2/2
✓ Branch 0 taken 24494 times.
✓ Branch 1 taken 2989 times.
27483 if (m_state != State::Opening) {
569 24494 return;
570 }
571
572 // Opening. Two things have to be true, and neither implies the other.
573 // Our stream must be linked into the graph -- until it is, pipewiresink
574 // drops whatever it is given. And the card must be RUNNING: PipeWire runs
575 // a newly linked chain on a placeholder driver for ~100 ms while the ALSA
576 // node is still opening, and audio fed then is consumed into nothing.
577 //
578 // If the card was idle when we opened, it starting is the proof of both:
579 // nothing else woke it. If something else (AES67, an input mix) already
580 // had it running, that proves nothing about us, so ask pw-link -- as for a
581 // sink that reaches no card at all (a group of network senders only),
582 // where there is nothing to watch but the link.
583 //
584 // (The stream's GstPipeWireClock would be the natural thing to watch, but
585 // it does not advance at all for a follower of an ALSA-driven graph --
586 // pw_stream_get_time_n() reports a zero rate and the clock returns its last
587 // value -- so nothing here runs on it.)
588 2989 bool ready = false;
589 //
590 // No settling time after the card starts is needed: a cold-start sweep on
591 // an AM335x with a USB card, checking what actually crossed the bus to the
592 // card, kept the head of the track intact with none, and 25-400 ms bought
593 // nothing.
594
6/6
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 2670 times.
✓ Branch 2 taken 134 times.
✓ Branch 3 taken 185 times.
✓ Branch 4 taken 134 times.
✓ Branch 5 taken 2855 times.
2989 if (!m_cardStatusPath.empty() && !m_cardWasRunning) {
595
1/2
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
134 ready = CardRunning();
596 } else {
597
7/8
✓ Branch 0 taken 773 times.
✓ Branch 1 taken 2082 times.
✓ Branch 2 taken 773 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 256 times.
✓ Branch 5 taken 517 times.
✓ Branch 6 taken 256 times.
✓ Branch 7 taken 2599 times.
2855 if (!m_linked && NowMs() - m_lastLinkCheckMs >= (uint64_t)kLinkCheckIntervalMs) {
598 // pw-link talks to the daemon; don't hold the lock the feed and
599 // Acquire() need while it does.
600
3/6
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 256 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 256 times.
✗ Branch 5 not taken.
256 GstElement* pipeline = GST_ELEMENT(gst_object_ref(m_pipeline));
601
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 lock.unlock();
602
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 bool linked = StreamLinked();
603
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 lock.lock();
604 256 bool same = (m_pipeline == pipeline);
605
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 gst_object_unref(pipeline);
606
2/4
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 256 times.
256 if (!same || m_state != State::Opening) {
607 ✗ return;
608 }
609
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 m_lastLinkCheckMs = NowMs();
610
2/2
✓ Branch 0 taken 136 times.
✓ Branch 1 taken 120 times.
256 if (linked) {
611 136 m_linked = true;
612
1/2
✓ Branch 0 taken 136 times.
✗ Branch 1 not taken.
136 m_linkedMs = NowMs();
613 }
614 }
615
7/8
✓ Branch 0 taken 2218 times.
✓ Branch 1 taken 637 times.
✓ Branch 2 taken 2218 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 133 times.
✓ Branch 5 taken 2085 times.
✓ Branch 6 taken 8 times.
✓ Branch 7 taken 125 times.
2996 ready = m_linked && NowMs() - m_linkedMs >= (uint64_t)kNoCardSettleMs &&
616
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
141 (m_cardStatusPath.empty() || CardRunning());
617 }
618
1/2
✓ Branch 0 taken 2989 times.
✗ Branch 1 not taken.
2989 now = NowMs();
619
2/2
✓ Branch 0 taken 147 times.
✓ Branch 1 taken 2842 times.
2989 if (ready) {
620 147 m_state = State::Ready;
621
1/2
✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
147 LogInfo(VB_MEDIAOUT, "PipeWire output stream %d ready on '%s' after %d ms\n",
622 m_slot, m_target.c_str(), (int)(now - m_openStartedMs));
623 147 m_stateChanged.notify_all();
624
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2842 times.
2842 } else if (now - m_openStartedMs > (uint64_t)kOpenTimeoutMs) {
625 ✗ LogWarn(VB_MEDIAOUT, "PipeWire output stream %d: not running after %d ms (linked %s, card %s), giving up\n",
626 m_slot, kOpenTimeoutMs, m_linked ? "yes" : "no",
627 m_cardStatusPath.empty() ? "n/a" : (CardRunning() ? "running" : "not running"));
628 ✗ m_wantOpen = false;
629 ✗ CloseLocked("open timed out");
630 ✗ m_state = State::Failed;
631 ✗ m_failedMs = now;
632 ✗ m_stateChanged.notify_all();
633 }
634
2/2
✓ Branch 0 taken 2989 times.
✓ Branch 1 taken 25513 times.
28502 }
635
636 203 bool PipeWireOutputStream::OpenLocked(std::unique_lock<std::mutex>& lock) {
637
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 std::string target = m_wantTarget;
638
639 // Nothing below touches this object's state until the pipeline is
640 // running, and all of it can be slow -- gst_init() on first use (~20 s
641 // the first time a box ever runs GStreamer), and pipewiresink connecting
642 // to the daemon -- so none of it runs under the slot lock, which the feed
643 // callback and Acquire() on the playlist thread both need.
644
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 lock.unlock();
645
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 GStreamerOutput::EnsureGStreamerInit();
646
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 int rate = GStreamerOutput::PipeWireGraphRate();
647
2/4
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
203 std::string capsStr = "audio/x-raw" + CapsFields();
648 203 std::string launch = std::string("appsrc name=src is-live=true format=time do-timestamp=false block=false "
649
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
406 "max-time=1000000000 leaky-type=downstream caps=\"") +
650
2/4
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
203 capsStr + "\" ! pipewiresink name=pwsink sync=false";
651 203 GError* error = nullptr;
652
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 GstElement* pipeline = gst_parse_launch(launch.c_str(), &error);
653
2/4
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 203 times.
203 if (error || !pipeline) {
654 ✗ LogErr(VB_MEDIAOUT, "PipeWire output stream %d: could not build: %s\n", m_slot,
655 error ? error->message : "unknown error");
656 ✗ if (error) {
657 ✗ g_error_free(error);
658 }
659 ✗ if (pipeline) {
660 ✗ gst_object_unref(pipeline);
661 }
662 ✗ lock.lock();
663 ✗ return false;
664 }
665 // The identity every per-track pipewiresink used to present, so anything
666 // that matches or routes fppd_stream_N sees no difference.
667
4/8
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 203 times.
✗ Branch 7 not taken.
203 if (GstElement* pwsink = gst_bin_get_by_name(GST_BIN(pipeline), "pwsink")) {
668 // The target is a property, not part of the description: it comes
669 // from the PipeWireSinkName setting, and pasted in, a crafted value
670 // added elements to the pipeline.
671
2/2
✓ Branch 0 taken 152 times.
✓ Branch 1 taken 51 times.
203 if (!target.empty()) {
672
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 g_object_set(pwsink, "target-object", target.c_str(), nullptr);
673 }
674
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 std::string nodeName = StreamSlotManager::GetNodeName(m_slot);
675
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 std::string nodeDesc = StreamSlotManager::GetNodeDescription(m_slot);
676
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 GstStructure* props = gst_structure_new("props",
677 "node.name", G_TYPE_STRING, nodeName.c_str(),
678 "node.description", G_TYPE_STRING, nodeDesc.c_str(),
679 NULL);
680
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 g_object_set(pwsink, "stream-properties", props, NULL);
681
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 gst_structure_free(props);
682
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 gst_object_unref(pwsink);
683 203 }
684
3/6
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
203 GstElement* src = gst_bin_get_by_name(GST_BIN(pipeline), "src");
685
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 std::string cardStatus = GStreamerOutput::AlsaSinkStatusPath(target);
686
4/4
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 181 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 14 times.
225 bool cardWasRunning = !cardStatus.empty() &&
687
3/6
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 181 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
225 GetFileContents(cardStatus).find("state: RUNNING") != std::string::npos;
688
689 // The card clock, like every pipeline feeding this one, so that the base
690 // times OnFeedSample() maps between are on one clock (see the note in
691 // Service() on why not pipewiresink's own).
692
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 GstClock* clock = Clock();
693
3/6
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
203 gst_pipeline_use_clock(GST_PIPELINE(pipeline), clock);
694
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 gst_object_unref(clock);
695
696
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 uint64_t started = NowMs();
697
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 GstStateChangeReturn ret = gst_element_set_state(pipeline, GST_STATE_PLAYING);
698
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 lock.lock();
699
700
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 153 times.
203 if (ret == GST_STATE_CHANGE_FAILURE) {
701
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 LogErr(VB_MEDIAOUT, "PipeWire output stream %d: could not start on '%s'\n", m_slot, target.c_str());
702
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 gst_object_unref(src);
703 50 std::thread([pipeline]() {
704 50 gst_element_set_state(pipeline, GST_STATE_NULL);
705 50 gst_object_unref(pipeline);
706
2/4
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✗ Branch 3 not taken.
50 }).detach();
707 50 return false;
708 }
709
710 153 m_pipeline = pipeline;
711 153 m_src = src;
712
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 GstCaps* fixedCaps = gst_caps_from_string(capsStr.c_str());
713
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 gst_caps_replace(&m_caps, fixedCaps);
714
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 gst_caps_unref(fixedCaps);
715 153 m_rate = rate;
716 153 m_warnedCaps = false;
717 153 m_queuedUntilUs = 0;
718 // The silence that connects the stream counts: the first track is
719 // flushed too, so it starts from the same state as every later one.
720 153 m_flushed = false;
721
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 m_target = target;
722
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 m_cardStatusPath = cardStatus;
723 153 m_cardWasRunning = cardWasRunning;
724 153 m_openStartedMs = started;
725 153 m_linked = false;
726 153 m_linkedMs = 0;
727 153 m_lastLinkCheckMs = 0;
728 153 m_state = State::Opening;
729
5/6
✓ Branch 0 taken 131 times.
✓ Branch 1 taken 22 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 152 times.
✓ Branch 4 taken 153 times.
✗ Branch 5 not taken.
153 LogInfo(VB_MEDIAOUT, "PipeWire output stream %d opening on '%s' (card status %s)\n", m_slot,
730 target.empty() ? "(default)" : target.c_str(), cardStatus.empty() ? "n/a" : cardStatus.c_str());
731 // pipewiresink only connects its stream once it has caps, which arrive
732 // with the first buffer.
733
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 PushSilenceLocked(20);
734 153 return true;
735 203 }
736
737 2308 void PipeWireOutputStream::PushSilenceLocked(int ms) {
738
3/4
✓ Branch 0 taken 2306 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2306 times.
2308 if (!m_src || m_rate <= 0) {
739 2 return;
740 }
741 2306 size_t frames = (size_t)m_rate * ms / 1000;
742 2306 GstBuffer* buf = gst_buffer_new_and_alloc(frames * kBytesPerFrame);
743 2306 gst_buffer_memset(buf, 0, 0, frames * kBytesPerFrame);
744 2306 GstClockTime now = gst_clock_get_time(m_clock);
745 2306 GstClockTime base = gst_element_get_base_time(m_pipeline);
746
1/2
✓ Branch 0 taken 2306 times.
✗ Branch 1 not taken.
2306 GST_BUFFER_PTS(buf) = now > base ? now - base : 0;
747 2306 GST_BUFFER_DURATION(buf) = gst_util_uint64_scale(frames, GST_SECOND, m_rate);
748 2306 NoteQueuedLocked(frames * kBytesPerFrame);
749 2306 gst_app_src_push_buffer(GST_APP_SRC(m_src), buf);
750 }
751
752 // Whether this slot's stream has an output port linked to anything, per
753 // pw-link. Only run while a stream is opening, every kLinkCheckIntervalMs.
754 // Time-limited, like the other PipeWire tools fppd runs: a pw-link that never
755 // answers (a wedged daemon) would otherwise hold this worker for good, and
756 // shutdown joins it.
757 256 bool PipeWireOutputStream::StreamLinked() {
758
2/4
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 256 times.
✗ Branch 3 not taken.
256 std::string prefix = StreamSlotManager::GetNodeName(m_slot) + ":";
759
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 FILE* f = popen("timeout -k 1 2 pw-link -o -l 2>/dev/null", "r");
760
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 256 times.
256 if (!f) {
761 ✗ return false;
762 }
763 // Output ports are listed at column 0, each followed by its links
764 // indented as " |-> peer:port".
765 256 bool ours = false;
766 256 bool linked = false;
767 char line[512];
768
3/4
✓ Branch 0 taken 2273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2017 times.
✓ Branch 3 taken 256 times.
2273 while (fgets(line, sizeof(line), f)) {
769
2/2
✓ Branch 0 taken 1396 times.
✓ Branch 1 taken 621 times.
2017 if (line[0] != ' ') {
770 1396 ours = strncmp(line, prefix.c_str(), prefix.size()) == 0;
771
3/4
✓ Branch 0 taken 291 times.
✓ Branch 1 taken 330 times.
✓ Branch 2 taken 291 times.
✗ Branch 3 not taken.
621 } else if (ours && strstr(line, "|->")) {
772 291 linked = true;
773 }
774 }
775
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 pclose(f);
776 256 return linked;
777 256 }
778
779 // ---------------------------------------------------------------------------
780 // The card clock
781 //
782 // Nothing in this stream resamples: PipeWire takes the stream's samples at
783 // the card's pace, which is whatever the card's crystal makes it -- 432 ppm
784 // fast on an AM62x I2S cape, 47 ppm slow on a Pi 5 HAT, tens of ppm either way
785 // for USB. A feed paced by the system clock delivers at the system's pace
786 // instead. A fast card then drains the stream's cushion (one quantum) and
787 // plays a quantum of silence: on the 432 ppm cape, 12 ms every 27 s. A slow
788 // one builds a queue that delays the sound further behind the lights for as
789 // long as the stream is fed.
790 //
791 // So every pipeline feeding the stream runs on this clock, which is held to
792 // the card. Its rate is fitted to the card's own hardware pointer (hw_ptr in
793 // /proc/asound/.../status, which ALSA updates from the DMA position when the
794 // file is read) against CLOCK_MONOTONIC, the clock's internal time, over the
795 // last two minutes. On top of that, any accumulated phase error is closed
796 // over ~20 s, so the queue between the feed and the card stays where it
797 // started instead of wandering with every error in the fit. The media
798 // position the lights follow comes from the feed, so it is card time too;
799 // ClockRate() lets the channel output thread run its frames at the same pace.
800 //
801 // Without a card (a group of network sinks) the clock is left at the system
802 // rate.
803 // ---------------------------------------------------------------------------
804
805 2084 void PipeWireOutputStream::EnsureClocksLocked() {
806
2/2
✓ Branch 0 taken 163 times.
✓ Branch 1 taken 1921 times.
2084 if (!m_clock) {
807
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 std::string name = "fpp-card-clock-" + std::to_string(m_slot);
808
4/8
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 163 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 163 times.
✗ Branch 7 not taken.
163 m_clock = GST_CLOCK(g_object_new(GST_TYPE_SYSTEM_CLOCK, "name", name.c_str(),
809 "clock-type", GST_CLOCK_TYPE_MONOTONIC, NULL));
810
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 gst_object_ref_sink(m_clock);
811 163 }
812
2/2
✓ Branch 0 taken 163 times.
✓ Branch 1 taken 1921 times.
2084 if (!m_feedClock) {
813 // Both read CLOCK_MONOTONIC as their internal time, and start out
814 // calibrated alike, so at speed 1 the feed clock reads exactly what
815 // the card clock does.
816
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 std::string name = "fpp-feed-clock-" + std::to_string(m_slot);
817
4/8
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 163 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 163 times.
✗ Branch 7 not taken.
163 m_feedClock = GST_CLOCK(g_object_new(GST_TYPE_SYSTEM_CLOCK, "name", name.c_str(),
818 "clock-type", GST_CLOCK_TYPE_MONOTONIC, NULL));
819
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 gst_object_ref_sink(m_feedClock);
820 GstClockTime cInt, cExt, num, den;
821
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 gst_clock_get_calibration(m_clock, &cInt, &cExt, &num, &den);
822
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 gst_clock_set_calibration(m_feedClock, cInt, cExt, num, den);
823 163 }
824 2084 }
825
826 203 GstClock* PipeWireOutputStream::Clock() {
827
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 std::unique_lock<std::mutex> lock(m_lock);
828
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 EnsureClocksLocked();
829
3/6
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
406 return GST_CLOCK(gst_object_ref(m_clock));
830 203 }
831
832 1881 GstClock* PipeWireOutputStream::FeedClock() {
833
1/2
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
1881 std::unique_lock<std::mutex> lock(m_lock);
834
1/2
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
1881 EnsureClocksLocked();
835
3/6
✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1881 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1881 times.
✗ Branch 5 not taken.
3762 return GST_CLOCK(gst_object_ref(m_feedClock));
836 1881 }
837
838 10842 void PipeWireOutputStream::RecalibrateFeedClockLocked(GstClockTime internal) {
839
2/4
✓ Branch 0 taken 10842 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10842 times.
10842 if (!m_clock || !m_feedClock) {
840 6966 return;
841 }
842 GstClockTime cInt, cExt, num, den;
843
1/2
✓ Branch 0 taken 10842 times.
✗ Branch 1 not taken.
10842 gst_clock_get_calibration(m_clock, &cInt, &cExt, &num, &den);
844
2/2
✓ Branch 0 taken 6966 times.
✓ Branch 1 taken 3876 times.
10842 if (!m_feedDiverged) {
845 // Not yet steered: keep it the card clock exactly, no rounding.
846
1/2
✓ Branch 0 taken 6966 times.
✗ Branch 1 not taken.
6966 gst_clock_set_calibration(m_feedClock, cInt, cExt, num, den);
847 6966 return;
848 }
849 // Where the feed clock is now on its current calibration, from here on
850 // running at the card clock's rate times the speed: continuous at
851 // `internal`, so a feed waiting on it never sees it step.
852 GstClockTime fInt, fExt, fNum, fDen;
853
1/2
✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
3876 gst_clock_get_calibration(m_feedClock, &fInt, &fExt, &fNum, &fDen);
854
1/2
✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
3876 GstClockTime external = gst_clock_adjust_with_calibration(m_feedClock, internal, fInt, fExt, fNum, fDen);
855
1/2
✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
3876 double rate = (double)num / (double)(den ? den : 1) * m_feedSpeed;
856
1/2
✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
3876 gst_clock_set_calibration(m_feedClock, internal, external, (GstClockTime)llround(rate * 1e9), 1000000000ULL);
857 }
858
859 968 void PipeWireOutputStream::SetFeedSpeed(GstElement* feed, double speed) {
860
1/2
✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
968 std::unique_lock<std::mutex> lock(m_lock);
861
5/10
✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 968 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 968 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 968 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 968 times.
968 if (!feed || feed != m_feed || !m_clock || !m_feedClock || speed <= 0.0) {
862 ✗ return;
863 }
864
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 968 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
968 if (speed == m_feedSpeed && m_feedDiverged) {
865 ✗ return;
866 }
867 // A speed of 1 before any other leaves everything untouched.
868
3/4
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 952 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
968 if (speed == 1.0 && !m_feedDiverged) {
869 ✗ return;
870 }
871
1/2
✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
968 GstClockTime internal = gst_clock_get_internal_time(m_clock);
872
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 953 times.
968 if (!m_feedDiverged) {
873 15 m_feedDiverged = true;
874 15 m_rsActive = true;
875 // The next output frame is the next buffer's first: everything up to
876 // it has gone out as it came, and m_rsHist holds the frames before.
877 15 m_rsPos = 0.0;
878 }
879 // Recalibrate at the old speed first (a no-op continuation), then switch.
880
1/2
✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
968 RecalibrateFeedClockLocked(internal);
881 968 m_feedSpeed = speed;
882
1/2
✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
968 RecalibrateFeedClockLocked(internal);
883
1/2
✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
968 }
884
885 16 void PipeWireOutputStream::FlushFeed(GstElement* feed) {
886 {
887
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 std::unique_lock<std::mutex> lock(m_lock);
888
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
16 if (!feed || feed != m_feed) {
889 ✗ return;
890 }
891 // What comes next doesn't follow on from what was resampled before.
892 16 m_rsHistFrames = 0;
893 16 m_rsPos = 0.0;
894
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 }
895 // Unprimed, as for an xrun: the seek is already a jump in the sound, and
896 // priming would only put silence ahead of where the feed lands.
897 16 Flush(false);
898 }
899
900 // The feed's resampler (SetFeedSpeed()): a 16-tap Kaiser-windowed sinc, its
901 // fractional delay taken from a 256-phase table with linear interpolation
902 // between phases. A speed servo only moves the ratio a few tenths of a
903 // percent from 1, but every sample then sits at a fraction that sweeps
904 // through the whole range, and a short interpolator's response changes with
905 // that fraction: a 4-point cubic measured -33 dB THD+N on an 8 kHz tone,
906 // audible as warbling highs. This one measures -81 dB at 1 kHz, -88 dB at
907 // 8 kHz and -74 dB at 15 kHz (44.1 kHz, ratios 0.9995-1.003), and with NEON
908 // costs 1.7% of a BBB's core (0.3% of a PocketBeagle2's) for 44.1 kHz stereo.
909 namespace {
910 constexpr int kRsTaps = 16;
911 constexpr int kRsHalf = kRsTaps / 2;
912 constexpr int kRsPhases = 256;
913 constexpr int kRsHist = kRsTaps - 1; // frames before the next buffer it reaches back to
914 constexpr double kRsKaiserBeta = 7.0;
915
916 12434 const float* ResampleTable() {
917 20 static const std::vector<float> table = [] {
918 82200 auto bessel0 = [](double x) {
919 82200 double sum = 1.0, term = 1.0;
920
2/2
✓ Branch 0 taken 2383800 times.
✓ Branch 1 taken 82200 times.
2466000 for (int k = 1; k < 30; k++) {
921 2383800 term *= (x / (2 * k)) * (x / (2 * k));
922 2383800 sum += term;
923 }
924 82200 return sum;
925 };
926
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 std::vector<float> t((kRsPhases + 1) * kRsTaps);
927
2/2
✓ Branch 0 taken 2570 times.
✓ Branch 1 taken 10 times.
2580 for (int ph = 0; ph <= kRsPhases; ph++) {
928 2570 double frac = (double)ph / kRsPhases;
929 2570 double sum = 0.0;
930
2/2
✓ Branch 0 taken 41120 times.
✓ Branch 1 taken 2570 times.
43690 for (int k = 0; k < kRsTaps; k++) {
931 // Tap k reads input frame i + k - (kRsHalf - 1).
932 41120 double x = (double)(k - (kRsHalf - 1)) - frac;
933
2/2
✓ Branch 0 taken 41100 times.
✓ Branch 1 taken 20 times.
41120 double sinc = x == 0.0 ? 1.0 : std::sin(M_PI * x) / (M_PI * x);
934 41120 double r = x / kRsHalf;
935
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 41100 times.
41120 double w = std::fabs(r) >= 1.0 ? 0.0 : bessel0(kRsKaiserBeta * std::sqrt(1.0 - r * r)) / bessel0(kRsKaiserBeta);
936 41120 t[ph * kRsTaps + k] = (float)(sinc * w);
937 41120 sum += sinc * w;
938 }
939
2/2
✓ Branch 0 taken 41120 times.
✓ Branch 1 taken 2570 times.
43690 for (int k = 0; k < kRsTaps; k++) {
940 41120 t[ph * kRsTaps + k] = (float)(t[ph * kRsTaps + k] / sum); // unity gain at DC
941 }
942 }
943 10 return t;
944
4/8
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 12424 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
12434 }();
945 12434 return table.data();
946 }
947 }
948
949 53322 void PipeWireOutputStream::KeepResampleHistoryLocked(GstBuffer* in) {
950 GstMapInfo map;
951
2/4
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 53322 times.
53322 if (!gst_buffer_map(in, &map, GST_MAP_READ)) {
952 ✗ return;
953 }
954 53322 const float* x = reinterpret_cast<const float*>(map.data);
955 53322 int n = (int)(map.size / kBytesPerFrame);
956 53322 int take = std::min(n, kRsHist);
957 // Shift out what the new frames replace, then append them.
958 53322 int keep = std::min(m_rsHistFrames, kRsHist - take);
959 53322 std::memmove(m_rsHist, m_rsHist + (m_rsHistFrames - keep) * kChannels, keep * kChannels * sizeof(float));
960 53322 std::memcpy(m_rsHist + keep * kChannels, x + (n - take) * kChannels, take * kChannels * sizeof(float));
961 53322 m_rsHistFrames = keep + take;
962
1/2
✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
53322 gst_buffer_unmap(in, &map);
963 }
964
965 12434 GstBuffer* PipeWireOutputStream::ResampleLocked(GstBuffer* in) {
966 static_assert(sizeof(m_rsHist) == kRsHist * kChannels * sizeof(float), "m_rsHist holds kRsHist frames");
967 GstMapInfo im;
968
2/4
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 12434 times.
12434 if (!gst_buffer_map(in, &im, GST_MAP_READ)) {
969 ✗ return nullptr;
970 }
971 12434 const float* x = reinterpret_cast<const float*>(im.data);
972 12434 const long n = (long)(im.size / kBytesPerFrame);
973 12434 const double step = m_feedSpeed;
974
1/2
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
12434 const float* table = ResampleTable();
975 // Input frame i of this buffer, or of the history for i in -kRsHist..-1
976 // (a frame before the first of the track reads as silence).
977 12434 const int hist = m_rsHistFrames;
978 5962208 auto at = [&](long i, int c) -> float {
979
2/2
✓ Branch 0 taken 2982616 times.
✓ Branch 1 taken 2979592 times.
5962208 if (i >= 0) {
980 2982616 return x[i * kChannels + c];
981 }
982 2979592 long h = hist + i;
983
2/2
✓ Branch 0 taken 2978696 times.
✓ Branch 1 taken 896 times.
2979592 return h >= 0 ? m_rsHist[h * kChannels + c] : 0.0f;
984 12434 };
985 12434 size_t cap = (size_t)((double)(n + kRsHist) / step) + 4;
986
1/2
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
12434 GstBuffer* out = gst_buffer_new_allocate(nullptr, cap * kBytesPerFrame, nullptr);
987 GstMapInfo om;
988
4/8
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12434 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 12434 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 12434 times.
12434 if (!out || !gst_buffer_map(out, &om, GST_MAP_WRITE)) {
989 ✗ gst_buffer_unmap(in, &im);
990 ✗ if (out) {
991 ✗ gst_buffer_unref(out);
992 }
993 ✗ return nullptr;
994 }
995 12434 float* y = reinterpret_cast<float*>(om.data);
996 12434 size_t m = 0;
997 12434 double p = m_rsPos;
998 float coef[kRsTaps];
999
1/2
✓ Branch 0 taken 32532111 times.
✗ Branch 1 not taken.
32532111 while (m < cap) {
1000 32532111 long i = (long)std::floor(p);
1001
2/2
✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 32519677 times.
32532111 if (i + kRsHalf > n - 1) {
1002 12434 break;
1003 }
1004 32519677 double fph = (p - (double)i) * kRsPhases;
1005 32519677 int ph = std::min((int)fph, kRsPhases - 1);
1006 32519677 float fr = (float)(fph - ph);
1007 32519677 const float* c0 = table + ph * kRsTaps;
1008 32519677 const float* c1 = c0 + kRsTaps;
1009 32519677 long first = i - (kRsHalf - 1);
1010 #if defined(__ARM_NEON) || defined(__ARM_NEON__)
1011
2/2
✓ Branch 0 taken 31459217 times.
✓ Branch 1 taken 179286 times.
31638503 if (first >= 0) {
1012 // Both channels four taps at a time: 1.7% of a BBB's core for
1013 // 44.1 kHz stereo, against 8.7% for the plain loop below (GCC
1014 // won't vectorize a float sum without -ffast-math).
1015 static_assert(kChannels == 2 && kRsTaps % 4 == 0, "the NEON path is stereo, four taps a step");
1016 31459217 const float* xs = x + first * kChannels;
1017 31459217 float32x4_t accL = vdupq_n_f32(0.0f);
1018 31459217 float32x4_t accR = vdupq_n_f32(0.0f);
1019
2/2
✓ Branch 0 taken 125836868 times.
✓ Branch 1 taken 31459217 times.
157296085 for (int k = 0; k < kRsTaps; k += 4) {
1020
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
125836868 float32x4_t a = vld1q_f32(c0 + k);
1021
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
377510604 float32x4_t co = vmlaq_n_f32(a, vsubq_f32(vld1q_f32(c1 + k), a), fr);
1022
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
125836868 float32x4x2_t lr = vld2q_f32(xs + 2 * k);
1023
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
125836868 accL = vmlaq_f32(accL, lr.val[0], co);
1024
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
251673736 accR = vmlaq_f32(accR, lr.val[1], co);
1025 }
1026
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
94377651 float32x2_t l2 = vadd_f32(vget_low_f32(accL), vget_high_f32(accL));
1027
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
62918434 float32x2_t r2 = vadd_f32(vget_low_f32(accR), vget_high_f32(accR));
1028
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
62918434 vst1_f32(y + m * kChannels, vpadd_f32(l2, r2));
1029 31459217 m++;
1030 31459217 p += step;
1031 31459217 continue;
1032 31459217 }
1033 #endif
1034
2/2
✓ Branch 0 taken 16967360 times.
✓ Branch 1 taken 1060460 times.
18027820 for (int k = 0; k < kRsTaps; k++) {
1035 16967360 coef[k] = c0[k] + fr * (c1[k] - c0[k]);
1036 }
1037
2/2
✓ Branch 0 taken 2120920 times.
✓ Branch 1 taken 1060460 times.
3181380 for (int c = 0; c < kChannels; c++) {
1038 2120920 float acc = 0.0f;
1039
2/2
✓ Branch 0 taken 1748282 times.
✓ Branch 1 taken 372638 times.
2120920 if (first >= 0) {
1040 1748282 const float* xs = x + first * kChannels + c;
1041
2/2
✓ Branch 0 taken 27972512 times.
✓ Branch 1 taken 1748282 times.
29720794 for (int k = 0; k < kRsTaps; k++) {
1042 27972512 acc += coef[k] * xs[k * kChannels];
1043 }
1044 } else {
1045
2/2
✓ Branch 0 taken 5962208 times.
✓ Branch 1 taken 372638 times.
6334846 for (int k = 0; k < kRsTaps; k++) {
1046 5962208 acc += coef[k] * at(first + k, c);
1047 }
1048 }
1049 2120920 y[m * kChannels + c] = acc;
1050 }
1051 1060460 m++;
1052 1060460 p += step;
1053 }
1054 // At the exit p >= n - kRsHalf, so the next buffer reaches back no
1055 // further than the history's kRsHist frames.
1056 12434 m_rsPos = p - (double)n;
1057
1/2
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
12434 gst_buffer_unmap(out, &om);
1058
1/2
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
12434 gst_buffer_unmap(in, &im);
1059
1/2
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
12434 gst_buffer_set_size(out, (gssize)(m * kBytesPerFrame));
1060
1/2
✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
12434 KeepResampleHistoryLocked(in);
1061 12434 return out;
1062 }
1063
1064 21404 void PipeWireOutputStream::ResetClockSyncLocked() {
1065 21404 m_syncAnchored = false;
1066 21404 m_syncWindow.clear();
1067 21404 }
1068
1069 // `trigger` is when the stream was last started (trigger_time), "" if the
1070 // file has none: a restart changes it, hw_ptr wrapping doesn't.
1071 7029 static bool ReadCardPosition(const std::string& path, int64_t& hw, std::string& trigger) {
1072
1/2
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
7029 std::string status = GetFileContents(path);
1073
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 7025 times.
7029 if (status.find("state: RUNNING") == std::string::npos) {
1074 4 return false;
1075 }
1076 7025 std::size_t p = status.find("hw_ptr");
1077
3/6
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7025 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 7025 times.
7025 if (p == std::string::npos || (p = status.find(':', p)) == std::string::npos) {
1078 ✗ return false;
1079 }
1080 7025 hw = strtoll(status.c_str() + p + 1, nullptr, 10);
1081 7025 trigger.clear();
1082 7025 std::size_t t = status.find("trigger_time");
1083
3/6
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7025 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7025 times.
✗ Branch 5 not taken.
7025 if (t != std::string::npos && (t = status.find(':', t)) != std::string::npos) {
1084 7025 std::size_t e = status.find('\n', t);
1085
2/4
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7025 times.
✗ Branch 3 not taken.
7025 trigger = status.substr(t + 1, e == std::string::npos ? std::string::npos : e - t - 1);
1086
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 TrimWhiteSpace(trigger);
1087 }
1088 7025 return true;
1089 7029 }
1090
1091 // Where the card's hw_ptr wraps back to 0 (sw_params "boundary"), 0 if it
1092 // can't be read. ALSA picks the largest power-of-two multiple of the buffer
1093 // below LONG_MAX: 2^62 frames with a 64-bit kernel, but 2^30 with a 32-bit
1094 // one (a BBB) -- about 6 h at 48 kHz, which a card kept running across songs
1095 // reaches in a long show.
1096 2 static int64_t ReadCardBoundary(const std::string& statusPath) {
1097
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 std::string text = GetFileContents(statusPath.substr(0, statusPath.rfind('/')) + "/sw_params");
1098 2 std::size_t p = text.find("boundary");
1099
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
2 if (p == std::string::npos || (p = text.find(':', p)) == std::string::npos) {
1100 ✗ return 0;
1101 }
1102 2 return strtoll(text.c_str() + p + 1, nullptr, 10);
1103 2 }
1104
1105 28502 bool PipeWireOutputStream::DisciplineClock() {
1106 28502 std::string path;
1107 {
1108
1/2
✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
28502 std::unique_lock<std::mutex> lock(m_lock);
1109
8/8
✓ Branch 0 taken 28344 times.
✓ Branch 1 taken 158 times.
✓ Branch 2 taken 24166 times.
✓ Branch 3 taken 4178 times.
✓ Branch 4 taken 17042 times.
✓ Branch 5 taken 7124 times.
✓ Branch 6 taken 21378 times.
✓ Branch 7 taken 7124 times.
28502 if (!m_clock || m_state != State::Ready || m_cardStatusPath.empty()) {
1110
1/2
✓ Branch 0 taken 21378 times.
✗ Branch 1 not taken.
21378 ResetClockSyncLocked();
1111 21378 return false;
1112 }
1113
1/2
✓ Branch 0 taken 7124 times.
✗ Branch 1 not taken.
7124 GstClockTime nowInt = gst_clock_get_internal_time(m_clock);
1114
4/4
✓ Branch 0 taken 7098 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 95 times.
✓ Branch 3 taken 7003 times.
7124 if (m_syncAnchored && nowInt - m_syncLastInt < (GstClockTime)kClockSampleMs * GST_MSECOND) {
1115 95 return false;
1116 }
1117
1/2
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
7029 path = m_cardStatusPath;
1118
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 7007 times.
7029 if (path != m_syncPath) {
1119 // Another card: what was learnt about the last one doesn't apply.
1120
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 m_syncPath = path;
1121 22 m_syncCardRate = 0;
1122 22 m_clockRate = 1.0;
1123
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 ResetClockSyncLocked();
1124 }
1125
2/2
✓ Branch 0 taken 7029 times.
✓ Branch 1 taken 21473 times.
28502 }
1126
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 7007 times.
7029 if (m_syncCardRate <= 0) {
1127
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
22 std::string hwParams = path.substr(0, path.rfind('/')) + "/hw_params";
1128
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 std::string text = GetFileContents(hwParams);
1129 22 std::size_t p = text.find("rate:");
1130
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 m_syncCardRate = p == std::string::npos ? 0 : atoi(text.c_str() + p + 5);
1131
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (m_syncCardRate <= 0) {
1132 ✗ return false;
1133 }
1134
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
22 }
1135 // The card's position and the clock's internal time, as close together
1136 // as they can be read: the midpoint of the read.
1137
1/2
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
7029 GstClockTime before = gst_clock_get_internal_time(m_clock);
1138 7029 int64_t hw = 0;
1139 7029 std::string trigger;
1140
1/2
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
7029 bool running = ReadCardPosition(path, hw, trigger);
1141
1/2
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
7029 GstClockTime after = gst_clock_get_internal_time(m_clock);
1142 7029 GstClockTime internal = before + (after - before) / 2;
1143
1144
1/2
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
7029 std::unique_lock<std::mutex> lock(m_lock);
1145
3/6
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7029 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 7029 times.
7029 if (path != m_cardStatusPath || m_state != State::Ready) {
1146 ✗ return false;
1147 }
1148 // The card stopping, or restarting, while a track feeds the stream is an
1149 // xrun: PipeWire restarted the device, and what the feed delivered
1150 // meanwhile is now queued behind it (see Flush()).
1151 //
1152 // A stop or restart right after one of our own flushes is the flush's
1153 // doing, not lost time. Taken for an xrun, it was flushed again, which
1154 // restarted the card again: on a card that restarts on a flush, one xrun
1155 // -- or the flush before a track -- had the stream flushed every 250 ms
1156 // for as long as it played.
1157 7029 bool lostTime = false;
1158
1/2
✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
7029 bool ownRestart = NowMs() - m_lastFlushMs < kFlushRestartMs;
1159
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 7025 times.
7029 if (!running) {
1160
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
4 lostTime = m_syncAnchored && m_feed && !ownRestart;
1161
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 ResetClockSyncLocked();
1162 4 return lostTime;
1163 }
1164 // A restart resets hw_ptr and changes trigger_time. hw_ptr also goes back
1165 // when it wraps at the ALSA boundary, with the card running on -- on a
1166 // 32-bit kernel every ~6 h -- and taking that for an xrun flushed a song
1167 // in mid-play. A wrap is told apart by an unchanged trigger_time and an
1168 // advance across the boundary no longer than the time since the last
1169 // sample allows; the position is then carried on past it.
1170
4/4
✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 6980 times.
7025 bool restarted = m_syncAnchored && trigger != m_syncTrigger;
1171
6/6
✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 6980 times.
✓ Branch 3 taken 19 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 6978 times.
7025 if (m_syncAnchored && !restarted && hw < m_syncLastHw) {
1172
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 int64_t boundary = trigger.empty() ? 0 : ReadCardBoundary(path);
1173 2 double sinceS = (double)(internal - m_syncLastInt) / GST_SECOND;
1174
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 double advanceS = boundary > m_syncLastHw ? (double)(hw + boundary - m_syncLastHw) / m_syncCardRate : -1.0;
1175
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (advanceS >= 0.0 && advanceS <= sinceS + 1.0) {
1176 2 m_syncWrap += boundary;
1177
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogDebug(VB_MEDIAOUT, "PipeWire output stream %d: card position wrapped at %lld\n",
1178 m_slot, (long long)boundary);
1179 } else {
1180 ✗ restarted = true;
1181 }
1182 }
1183
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 m_syncTrigger = trigger;
1184 7025 int64_t pos = hw + m_syncWrap;
1185
1186 GstClockTime cInt, cExt, num, den;
1187
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 gst_clock_get_calibration(m_clock, &cInt, &cExt, &num, &den);
1188
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 GstClockTime external = gst_clock_adjust_with_calibration(m_clock, internal, cInt, cExt, num, den);
1189
1190
2/2
✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
7025 double cardS = m_syncAnchored ? (double)(pos - m_syncHw0) / m_syncCardRate : 0.0;
1191
2/2
✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
7025 double clockS = m_syncAnchored ? (double)(GstClockTimeDiff)(external - m_syncExt0) / GST_SECOND : 0.0;
1192 7025 double phase = cardS - clockS; // > 0: the card is ahead of the clock
1193
5/6
✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 6980 times.
✓ Branch 3 taken 19 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 6980 times.
7025 if (!m_syncAnchored || restarted || std::fabs(phase) > kClockMaxPhaseError) {
1194
6/10
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 17 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
45 lostTime = m_syncAnchored && m_feed && (restarted ? !ownRestart : phase < -kClockMaxPhaseError);
1195
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 26 times.
45 if (m_syncAnchored) {
1196
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 LogDebug(VB_MEDIAOUT, "PipeWire output stream %d: card clock re-anchored (phase %.1f ms)\n",
1197 m_slot, phase * 1000.0);
1198 }
1199 // Start (again) from here, keeping the rate learnt so far.
1200 45 m_syncAnchored = true;
1201 45 m_syncHw0 = pos;
1202 45 m_syncInt0 = internal;
1203 45 m_syncExt0 = external;
1204 45 m_syncWindow.clear();
1205 45 cardS = clockS = phase = 0.0;
1206 }
1207 7025 m_syncLastHw = hw;
1208 7025 m_syncLastInt = internal;
1209
1210 7025 double intS = (double)(internal - m_syncInt0) / GST_SECOND;
1211
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 m_syncWindow.emplace_back(intS, cardS);
1212
6/6
✓ Branch 0 taken 8482 times.
✓ Branch 1 taken 79 times.
✓ Branch 2 taken 1536 times.
✓ Branch 3 taken 6946 times.
✓ Branch 4 taken 1536 times.
✓ Branch 5 taken 7025 times.
8561 while (m_syncWindow.size() > 2 && intS - m_syncWindow.front().first > kClockWindowS) {
1213 1536 m_syncWindow.pop_front();
1214 }
1215 7025 double rate = m_clockRate.load(std::memory_order_relaxed);
1216
2/2
✓ Branch 0 taken 6478 times.
✓ Branch 1 taken 547 times.
7025 if (intS - m_syncWindow.front().first >= kClockMinFitS) {
1217 6478 double mx = 0, my = 0;
1218
2/2
✓ Branch 0 taken 1724233 times.
✓ Branch 1 taken 6478 times.
1730711 for (auto& [x, y] : m_syncWindow) {
1219 1724233 mx += x;
1220 1724233 my += y;
1221 }
1222 6478 mx /= m_syncWindow.size();
1223 6478 my /= m_syncWindow.size();
1224 6478 double sxy = 0, sxx = 0;
1225
2/2
✓ Branch 0 taken 1724233 times.
✓ Branch 1 taken 6478 times.
1730711 for (auto& [x, y] : m_syncWindow) {
1226 1724233 sxy += (x - mx) * (y - my);
1227 1724233 sxx += (x - mx) * (x - mx);
1228 }
1229
1/2
✓ Branch 0 taken 6478 times.
✗ Branch 1 not taken.
6478 if (sxx > 0) {
1230
1/2
✓ Branch 0 taken 6478 times.
✗ Branch 1 not taken.
6478 rate = std::clamp(sxy / sxx, 1.0 - kClockMaxOffset, 1.0 + kClockMaxOffset);
1231 6478 m_clockRate = rate;
1232 }
1233 }
1234
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 double applied = rate + std::clamp(phase * kClockPhaseGain, -kClockMaxPhaseSlew, kClockMaxPhaseSlew);
1235
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 applied = std::clamp(applied, 1.0 - kClockMaxOffset, 1.0 + kClockMaxOffset);
1236 // Continuous at `internal`: only the rate from here on changes.
1237
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 gst_clock_set_calibration(m_clock, internal, external, (GstClockTime)llround(applied * 1e9), 1000000000ULL);
1238
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 RecalibrateFeedClockLocked(internal);
1239
1/2
✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
7025 LogExcess(VB_MEDIAOUT, "PipeWire output stream %d: card %+.1f ppm, phase %+.2f ms, clock %+.1f ppm\n",
1240 m_slot, (rate - 1.0) * 1e6, phase * 1000.0, (applied - 1.0) * 1e6);
1241 7025 return lostTime;
1242 28502 }
1243
1244 142 bool PipeWireOutputStream::CardRunning() {
1245
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 142 times.
142 if (m_cardStatusPath.empty()) {
1246 ✗ return false;
1247 }
1248
1/2
✓ Branch 0 taken 142 times.
✗ Branch 1 not taken.
142 return GetFileContents(m_cardStatusPath).find("state: RUNNING") != std::string::npos;
1249 }
1250
1251 181 void PipeWireOutputStream::CloseLocked(const char* why) {
1252
2/2
✓ Branch 0 taken 153 times.
✓ Branch 1 taken 28 times.
181 if (m_pipeline) {
1253 153 LogInfo(VB_MEDIAOUT, "PipeWire output stream %d closing (%s)\n", m_slot, why);
1254 153 GstElement* pipeline = m_pipeline;
1255 153 GstElement* src = m_src;
1256 // Off this thread: a pipewiresink state change can block on the
1257 // daemon, and the worker services every slot.
1258 306 std::thread([pipeline, src]() {
1259
2/4
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 153 times.
✗ Branch 3 not taken.
153 SetThreadName("FPP-PWStreamEnd");
1260 153 gst_element_set_state(pipeline, GST_STATE_NULL);
1261
1/2
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
153 if (src) {
1262 153 gst_object_unref(src);
1263 }
1264 153 gst_object_unref(pipeline);
1265
2/4
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 153 times.
✗ Branch 3 not taken.
306 }).detach();
1266 }
1267 181 gst_caps_replace(&m_caps, nullptr);
1268 181 m_rate = 0;
1269 181 m_queuedUntilUs = 0;
1270 181 m_pipeline = nullptr;
1271 181 m_src = nullptr;
1272 // m_feed stays: it belongs to the track using the slot (AttachFeed() /
1273 // Release()), not to this stream. Clearing it here orphaned a track
1274 // whose start triggered a reopen -- a new target -- and the whole track
1275 // played silent. Its samples are dropped while there is no m_src.
1276 181 m_target.clear();
1277 181 m_linked = false;
1278 181 m_state = State::Closed;
1279 181 m_stateChanged.notify_all();
1280 181 }
1281
1282 #endif
1283