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_spa_pod_support.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 <QtCore/qdebug.h>
7
8#include <pipewire/version.h>
9#include <spa/pod/parser.h>
10#include <spa/param/format.h>
11
12#if __has_include(<spa/param/audio/raw-utils.h>)
13# include <spa/param/audio/raw-utils.h>
14#else
15# include "qpipewire_spa_compat_p.h"
16#endif
17
18#if PW_CHECK_VERSION(0, 3, 44)
19# include <spa/param/audio/iec958.h>
20#else
22static constexpr spa_format SPA_FORMAT_AUDIO_iec958Codec = spa_format(65542);
23static constexpr spa_media_subtype SPA_MEDIA_SUBTYPE_iec958 = spa_media_subtype(3);
24#endif
25
26QT_BEGIN_NAMESPACE
27
28namespace QtPipeWire {
29
30namespace {
31
32std::optional<std::variant<spa_audio_format, SpaEnum<spa_audio_format>>>
33parseSampleFormat(const spa_pod &pod)
34{
35 std::optional<spa_audio_format> format = spaParsePodPropertyScalar<spa_audio_format>(
36 pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_format);
37 if (format)
38 return format;
39
40 std::optional<SpaEnum<spa_audio_format>> choice =
41 spaParsePodPropertyChoice<spa_audio_format, SPA_CHOICE_Enum>(
42 pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_format);
43 if (choice)
44 return *choice;
45 return std::nullopt;
46}
47
48std::optional<std::variant<SpaRange<int>, int>> parseSamplingRates(const spa_pod &pod)
49{
50 std::optional<int> rate =
51 spaParsePodPropertyScalar<int>(pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_format);
52 if (rate)
53 return rate;
54
55 std::optional<SpaRange<int>> choice = spaParsePodPropertyChoice<int, SPA_CHOICE_Range>(
56 pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_rate);
57 if (choice)
58 return *choice;
59 return std::nullopt;
60}
61
62bool isIec958Device(const spa_pod &pod)
63{
64 return spaParsePodPropertyScalar<spa_media_subtype>(pod, SPA_TYPE_OBJECT_Format,
65 SPA_FORMAT_mediaSubtype)
66 == SPA_MEDIA_SUBTYPE_iec958;
67}
68
69bool isIec958PCMDevice(const spa_pod &pod)
70{
71 std::optional<spa_audio_iec958_codec> codec = spaParsePodPropertyScalar<spa_audio_iec958_codec>(
72 pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_iec958Codec);
73 if (codec)
74 return codec == spa_audio_iec958_codec::SPA_AUDIO_IEC958_CODEC_PCM;
75
76 std::optional<SpaEnum<spa_audio_iec958_codec>> choice =
77 spaParsePodPropertyChoice<spa_audio_iec958_codec, SPA_CHOICE_Enum>(
78 pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_iec958Codec);
79 if (choice)
80 return choice->defaultValue() == spa_audio_iec958_codec::SPA_AUDIO_IEC958_CODEC_PCM;
81 return false;
82}
83
84} // namespace
85
86std::optional<SpaObjectAudioFormat> SpaObjectAudioFormat::parse(const spa_pod_object *obj)
87{
88 struct spa_audio_info_raw info = {};
89 if (spa_format_audio_raw_parse(&obj->pod, &info) < 0)
90 return std::nullopt;
91
93 result.channelCount = int(info.channels);
94
95 bool isIec958 = isIec958Device(obj->pod);
96
97 if (info.format != spa_audio_format::SPA_AUDIO_FORMAT_UNKNOWN) {
98 result.sampleTypes = info.format;
99 } else if (isIec958) {
100 bool isIec958Pcm = isIec958PCMDevice(obj->pod);
101 if (isIec958Pcm) {
102 result.channelCount = 2; // IEC958 PCM is always stereo
103 result.sampleTypes = spa_audio_iec958_codec::SPA_AUDIO_IEC958_CODEC_PCM;
104 } else {
105 return std::nullopt;
106 }
107 } else {
108 auto optionalSampleFormat = parseSampleFormat(obj->pod);
109 if (!optionalSampleFormat)
110 return std::nullopt;
111
112 std::visit([&](auto &&arg) {
113 result.sampleTypes = std::forward<decltype(arg)>(arg);
114 }, *optionalSampleFormat);
115 }
116
117 if (info.rate != 0) {
118 result.rates = int(info.rate);
119 } else {
120 auto optionalSamplingRates = parseSamplingRates(obj->pod);
121 // note: some virtual devices don't expose any sampling rate
122 if (optionalSamplingRates) {
123 std::visit([&](auto arg) {
124 result.rates = arg;
125 }, *optionalSamplingRates);
126 }
127 }
128
129 if (isIec958) {
130 // IEC958 PCM is always stereo, and the POD won't contain any information about channel
131 // positioning.
132 } else if (!SPA_FLAG_IS_SET(info.flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
133 result.channelPositions = QList<spa_audio_channel>();
134 for (int channelIndex = 0; channelIndex != result.channelCount; ++channelIndex)
135 result.channelPositions->push_back(spa_audio_channel(info.position[channelIndex]));
136 } else {
137 // unpositionioned
138 }
139
140 return result;
141}
142
143std::optional<SpaObjectAudioFormat> SpaObjectAudioFormat::parse(const spa_pod *pod)
144{
145 if (spa_pod_is_object_type(pod, SPA_TYPE_OBJECT_Format)) {
146 const spa_pod_object *obj = reinterpret_cast<const spa_pod_object *>(pod);
147 return parse(obj);
148 }
149 return std::nullopt;
150}
151
152namespace {
153
154spa_audio_format toSpaAudioFormat(QAudioFormat::SampleFormat fmt)
155{
156 switch (fmt) {
157 case QAudioFormat::Int16:
158 return SPA_AUDIO_FORMAT_S16;
159 case QAudioFormat::Int32:
160 return SPA_AUDIO_FORMAT_S32;
161 case QAudioFormat::UInt8:
162 return SPA_AUDIO_FORMAT_U8;
163 case QAudioFormat::Float:
164 return SPA_AUDIO_FORMAT_F32;
165 default:
166 return SPA_AUDIO_FORMAT_UNKNOWN;
167 }
168}
169
170void initializeChannelPositions(spa_audio_info_raw &info, const QAudioFormat &fmt)
171{
172 using ChannelConfig = QAudioFormat::ChannelConfig;
173 const ChannelConfig cfg = fmt.channelConfig();
174
175 auto fillPositions = [&](QSpan<const spa_audio_channel> positions) {
176 std::copy(positions.begin(), positions.end(), std::begin(info.position));
177 };
178
179 switch (cfg) {
180 case ChannelConfig::ChannelConfigMono:
181 return fillPositions(channelPositionsMono);
182
183 case ChannelConfig::ChannelConfigStereo:
184 return fillPositions(channelPositionsStereo);
185 case ChannelConfig::ChannelConfig2Dot1:
186 return fillPositions(channelPositions2Dot1);
187 case ChannelConfig::ChannelConfig3Dot0:
188 return fillPositions(channelPositions3Dot0);
189 case ChannelConfig::ChannelConfig3Dot1:
190 return fillPositions(channelPositions3Dot1);
191 case ChannelConfig::ChannelConfigSurround5Dot0:
192 return fillPositions(channelPositions5Dot0);
193 case ChannelConfig::ChannelConfigSurround5Dot1:
194 return fillPositions(channelPositions5Dot1);
195 case ChannelConfig::ChannelConfigSurround7Dot0:
196 return fillPositions(channelPositions7Dot0);
197 case ChannelConfig::ChannelConfigSurround7Dot1:
198 return fillPositions(channelPositions7Dot1);
199 case ChannelConfig::ChannelConfigUnknown:
200 default: {
201#if !PW_CHECK_VERSION(0, 3, 33)
202 uint32_t SPA_AUDIO_CHANNEL_START_Aux = 0x1000;
203#endif
204
205 // now we're in speculative territory: ChannelConfig is a bitmask and isn't
206 // able to represent arbitrary channel configurations.
207 //
208 // as a "best effort", we can try to populate all channels as "Aux" channels
209 // depending on the channel count
210 std::iota(info.position, info.position + fmt.channelCount(),
211 uint32_t(SPA_AUDIO_CHANNEL_START_Aux));
212 return;
213 }
214 }
215}
216
217} // namespace
218
219spa_audio_info_raw asSpaAudioInfoRaw(const QAudioFormat &fmt)
220{
221 spa_audio_info_raw ret{
222 .format = toSpaAudioFormat(fmt.sampleFormat()),
223 .flags = {},
224 .rate = uint32_t(fmt.sampleRate()),
225 .channels = uint32_t(fmt.channelCount()),
226 .position = {},
227 };
228
229 initializeChannelPositions(ret, fmt);
230
231 return ret;
232}
233
234} // namespace QtPipeWire
235
236QT_END_NAMESPACE
spa_audio_info_raw asSpaAudioInfoRaw(const QAudioFormat &)
#define __has_include(x)
static constexpr spa_media_subtype SPA_MEDIA_SUBTYPE_iec958
static constexpr spa_format SPA_FORMAT_AUDIO_iec958Codec