Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qpipewire_audiostream.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
7#include <QtMultimedia/private/qpipewire_audiocontextmanager_p.h>
8#include <QtMultimedia/private/qpipewire_spa_pod_support_p.h>
9
10#include <spa/pod/builder.h>
11
12#if __has_include(<spa/param/audio/raw-utils.h>)
13# include <spa/param/audio/raw-utils.h>
14#else
15# include <QtMultimedia/private/qpipewire_spa_compat_p.h>
16#endif
17
18
19#ifndef PW_KEY_NODE_FORCE_QUANTUM
20# define PW_KEY_NODE_FORCE_QUANTUM "node.force-quantum"
21#endif
22
23#if !PW_CHECK_VERSION(0, 3, 50)
24extern "C" {
25int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size);
26}
27#endif
28
29#include <array>
30
31QT_BEGIN_NAMESPACE
32
33namespace QtPipeWire {
34
36{
37}
38
40
41void QPipewireAudioStream::createStream(QSpan<spa_dict_item> extraProperties,
42 std::optional<int32_t> hardwareBufferFrames,
43 const char *streamName, StreamType type)
44{
45 stream_events.version = PW_VERSION_STREAM_EVENTS;
46
47 switch (type) {
49 stream_events.process = [](void *userData) {
50 reinterpret_cast<QPipewireAudioStream *>(userData)->processRingbuffer();
51 };
52 break;
54 stream_events.process = [](void *userData) {
55 reinterpret_cast<QPipewireAudioStream *>(userData)->processCallback();
56 };
57 break;
58 default:
59 Q_UNREACHABLE_RETURN();
60 };
61
62 stream_events.state_changed = [](void *userData, pw_stream_state old, pw_stream_state state,
63 const char *error) {
64 reinterpret_cast<QPipewireAudioStream *>(userData)->stateChanged(old, state, error);
65 };
66
67 std::vector<spa_dict_item> properties{
68 { PW_KEY_MEDIA_TYPE, "Audio" },
69 };
70 properties.insert(properties.end(), extraProperties.begin(), extraProperties.end());
71
72 std::string quantumStr;
73 if (hardwareBufferFrames) {
74 quantumStr = std::to_string(*hardwareBufferFrames);
75 properties.push_back({
77 quantumStr.data(),
78 });
79 }
80
81 QAudioContextManager::withEventLoopLock([&] {
82 m_stream = PwStreamHandle{
83 pw_stream_new_simple(QAudioContextManager::getEventLoop(), streamName,
84 makeProperties(properties).release(), &stream_events, this),
85 };
86 });
87 if (!m_stream)
88 qWarning() << "pw_stream_new_simple failed" << make_error_code().message();
89}
90
91bool QPipewireAudioStream::connectStream(ObjectSerial target, spa_direction direction)
92{
93 int status = QAudioContextManager::withEventLoopLock([&] {
94 std::optional<ObjectId> targetNodeId =
95 QAudioContextManager::deviceMonitor().findObjectId(target);
96 if (!targetNodeId)
97 return -ENODEV;
98
99 bool deviceAlreadyRemoved = registerDeviceObserver(target);
100 if (!deviceAlreadyRemoved)
101 return -ENODEV;
102
103 std::array<uint8_t, 1024> buffer;
104
105 QT_WARNING_PUSH
106 QT_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
107 struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer.data(), uint32_t(buffer.size()));
108 QT_WARNING_POP
109 spa_audio_info_raw audioInfo = asSpaAudioInfoRaw(m_format);
110
111 std::array<const struct spa_pod *, 1> params{
112 spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audioInfo),
113 };
114
115 return pw_stream_connect(
116 m_stream.get(), direction, targetNodeId->value,
117 pw_stream_flags(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS
118 | PW_STREAM_FLAG_RT_PROCESS | PW_STREAM_FLAG_DONT_RECONNECT),
119 params.data(), params.size());
120 });
121
122 if (status < 0) {
123 qWarning() << "pw_stream_connect failed" << make_error_code(-status).message();
124 return false;
125 }
126
127 return true;
128}
129
131{
132 int status = QAudioContextManager::withEventLoopLock([&] {
133 return pw_stream_set_active(m_stream.get(), false);
134 });
135 if (status < 0)
136 qWarning() << "pw_stream_set_active failed" << make_error_code(-status).message();
137}
138
140{
141 int status = QAudioContextManager::withEventLoopLock([&] {
142 m_skipNextTickDiscontinuity = true;
143 return pw_stream_set_active(m_stream.get(), true);
144 });
145 if (status < 0)
146 qWarning() << "pw_stream_set_active failed" << make_error_code(-status).message();
147}
148
150{
151 int status = QAudioContextManager::withEventLoopLock([&] {
152 return pw_stream_disconnect(m_stream.get());
153 });
154 if (status < 0)
155 qWarning() << "pw_stream_disconnect failed" << make_error_code(-status).message();
156}
157
159{
160 finalizeStream(); // mark the stream as stopped
161 QAudioContextManager::withEventLoopLock([&] {
162 if (m_deviceRemovalObserver)
163 unregisterDeviceObserver();
164 m_stream = {};
165 m_self = {};
166 });
167}
168
170{
171 return bool(m_stream);
172}
173
174bool QPipewireAudioStream::registerDeviceObserver(ObjectSerial nodeSerial)
175{
176 m_deviceRemovalObserver = std::make_shared<ObjectRemoveObserver>(nodeSerial);
177 QObject::connect(m_deviceRemovalObserver.get(), &ObjectRemoveObserver::objectRemoved,
178 m_deviceRemovalObserver.get(), [this] {
179 handleDeviceRemoved();
180 });
181
182 return QAudioContextManager::deviceMonitor().registerObserver(m_deviceRemovalObserver);
183}
184
186{
187 Q_ASSERT(m_deviceRemovalObserver);
188 QAudioContextManager::deviceMonitor().unregisterObserver(m_deviceRemovalObserver);
189 m_deviceRemovalObserver = {};
190}
191
193{
194 // XRun detection does not work well with pause/resume.
195 // disabling for now, since we don't have any public API for notifying the application about
196 // xruns
197 constexpr bool runXRunDetection = false;
198 if (!runXRunDetection)
199 return;
200
201 struct pw_time time_info = {};
203 if (status < 0) {
204 if (pw_check_library_version(0, 3, 50))
205 return; // no xrun detection on ancient pipewire
206
207 qFatal() << "pw_stream_get_time_n failed. This should not happen";
208 return;
209 }
210
212 // prevent xrun detection to fire after resume, as ticks will continue incrementing
214 } else if (std::abs(int64_t(m_expectedNextTick) - int64_t(time_info.ticks)) > 1024) {
216 m_xrunCount += 1;
218 }
219
220 // CAVEAT:
221 // counts `ticks` in the device rates, which may be different to the rate the stream is
222 // running in. We therefore cannot do any precise xrun detection with this technique, but
223 // only to a best effort.
224 // TODO: can we use profiler events?
226
227#if PW_CHECK_VERSION(1, 1, 0)
228 if (pw_check_library_version(1, 1, 0)) {
229 // LATER: rely on time_info.size, once 1.1 is the minimum required version
231 return;
232 }
233#endif
235}
236
241
242} // namespace QtPipeWire
243
244QT_END_NAMESPACE
#define __has_include(x)
#define PW_KEY_NODE_FORCE_QUANTUM
void createStream(QSpan< spa_dict_item > extraProperties, std::optional< int32_t > hardwareBufferFrames, const char *streamName, StreamType=StreamType::Ringbuffer)
virtual void stateChanged(pw_stream_state oldState, pw_stream_state state, const char *error)=0
virtual void processRingbuffer()=0
bool connectStream(ObjectSerial target, spa_direction)