Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qpipewire_spa_pod_support_p.h"
#include <QtCore/qdebug.h>
#include <pipewire/version.h>
#include <spa/pod/parser.h>
#include <spa/param/format.h>
#if __has_include(<spa/param/audio/raw-utils.h>)
# include <spa/param/audio/raw-utils.h>
#else
# include <QtMultimedia/private/qpipewire_spa_compat_p.h>
#endif
#if PW_CHECK_VERSION(0, 3, 44)
# include <spa/param/audio/iec958.h>
#else
# include <QtMultimedia/private/qpipewire_spa_compat_p.h>
static constexpr spa_format SPA_FORMAT_AUDIO_iec958Codec = spa_format(65542);
static constexpr spa_media_subtype SPA_MEDIA_SUBTYPE_iec958 = spa_media_subtype(3);
#endif
QT_BEGIN_NAMESPACE
namespace QtPipeWire {
namespace {
std::optional<std::variant<spa_audio_format, SpaEnum<spa_audio_format>>>
parseSampleFormat(const spa_pod &pod)
{
std::optional<spa_audio_format> format = spaParsePodPropertyScalar<spa_audio_format>(
pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_format);
if (format)
return format;
std::optional<SpaEnum<spa_audio_format>> choice =
spaParsePodPropertyChoice<spa_audio_format, SPA_CHOICE_Enum>(
pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_format);
if (choice)
return *choice;
return std::nullopt;
}
std::optional<std::variant<SpaRange<int>, int>> parseSamplingRates(const spa_pod &pod)
{
std::optional<int> rate =
spaParsePodPropertyScalar<int>(pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_rate);
if (rate)
return rate;
std::optional<SpaRange<int>> choice = spaParsePodPropertyChoice<int, SPA_CHOICE_Range>(
pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_rate);
if (choice)
return *choice;
return std::nullopt;
}
bool isIec958Device(const spa_pod &pod)
{
return spaParsePodPropertyScalar<spa_media_subtype>(pod, SPA_TYPE_OBJECT_Format,
SPA_FORMAT_mediaSubtype)
== SPA_MEDIA_SUBTYPE_iec958;
}
bool isIec958PCMDevice(const spa_pod &pod)
{
std::optional<spa_audio_iec958_codec> codec = spaParsePodPropertyScalar<spa_audio_iec958_codec>(
pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_iec958Codec);
if (codec)
return codec == spa_audio_iec958_codec::SPA_AUDIO_IEC958_CODEC_PCM;
std::optional<SpaEnum<spa_audio_iec958_codec>> choice =
spaParsePodPropertyChoice<spa_audio_iec958_codec, SPA_CHOICE_Enum>(
pod, SPA_TYPE_OBJECT_Format, SPA_FORMAT_AUDIO_iec958Codec);
if (choice)
return choice->defaultValue() == spa_audio_iec958_codec::SPA_AUDIO_IEC958_CODEC_PCM;
return false;
}
} // namespace
std::optional<SpaObjectAudioFormat> SpaObjectAudioFormat::parse(const spa_pod_object *obj)
{
struct spa_audio_info_raw info = {};
if (spa_format_audio_raw_parse(&obj->pod, &info) < 0)
return std::nullopt;
SpaObjectAudioFormat result;
result.channelCount = int(info.channels);
bool isIec958 = isIec958Device(obj->pod);
if (info.format != spa_audio_format::SPA_AUDIO_FORMAT_UNKNOWN) {
result.sampleTypes = info.format;
} else if (isIec958) {
bool isIec958Pcm = isIec958PCMDevice(obj->pod);
if (isIec958Pcm) {
result.channelCount = 2; // IEC958 PCM is always stereo
result.sampleTypes = spa_audio_iec958_codec::SPA_AUDIO_IEC958_CODEC_PCM;
} else {
return std::nullopt;
}
} else {
auto optionalSampleFormat = parseSampleFormat(obj->pod);
if (!optionalSampleFormat)
return std::nullopt;
std::visit([&](auto &&arg) {
result.sampleTypes = std::forward<decltype(arg)>(arg);
}, *optionalSampleFormat);
}
if (info.rate != 0) {
result.rates = int(info.rate);
} else {
auto optionalSamplingRates = parseSamplingRates(obj->pod);
// note: some virtual devices don't expose any sampling rate
if (optionalSamplingRates) {
std::visit([&](auto arg) {
result.rates = arg;
}, *optionalSamplingRates);
}
}
if (isIec958) {
// IEC958 PCM is always stereo, and the POD won't contain any information about channel
// positioning.
} else if (!SPA_FLAG_IS_SET(info.flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
result.channelPositions = QList<spa_audio_channel>();
for (int channelIndex = 0; channelIndex != result.channelCount; ++channelIndex)
result.channelPositions->push_back(spa_audio_channel(info.position[channelIndex]));
} else {
// unpositionioned
}
return result;
}
std::optional<SpaObjectAudioFormat> SpaObjectAudioFormat::parse(const spa_pod *pod)
{
if (spa_pod_is_object_type(pod, SPA_TYPE_OBJECT_Format)) {
const spa_pod_object *obj = reinterpret_cast<const spa_pod_object *>(pod);
return parse(obj);
}
return std::nullopt;
}
namespace {
spa_audio_format toSpaAudioFormat(QAudioFormat::SampleFormat fmt)
{
switch (fmt) {
case QAudioFormat::Int16:
return SPA_AUDIO_FORMAT_S16;
case QAudioFormat::Int32:
return SPA_AUDIO_FORMAT_S32;
case QAudioFormat::UInt8:
return SPA_AUDIO_FORMAT_U8;
case QAudioFormat::Float:
return SPA_AUDIO_FORMAT_F32;
default:
return SPA_AUDIO_FORMAT_UNKNOWN;
}
}
void initializeChannelPositions(spa_audio_info_raw &info, const QAudioFormat &fmt)
{
using ChannelConfig = QAudioFormat::ChannelConfig;
const ChannelConfig cfg = fmt.channelConfig();
auto fillPositions = [&](QSpan<const spa_audio_channel> positions) {
std::copy(positions.begin(), positions.end(), std::begin(info.position));
};
switch (cfg) {
case ChannelConfig::ChannelConfigMono:
return fillPositions(channelPositionsMono);
case ChannelConfig::ChannelConfigStereo:
return fillPositions(channelPositionsStereo);
case ChannelConfig::ChannelConfig2Dot1:
return fillPositions(channelPositions2Dot1);
case ChannelConfig::ChannelConfig3Dot0:
return fillPositions(channelPositions3Dot0);
case ChannelConfig::ChannelConfig3Dot1:
return fillPositions(channelPositions3Dot1);
case ChannelConfig::ChannelConfigSurround5Dot0:
return fillPositions(channelPositions5Dot0);
case ChannelConfig::ChannelConfigSurround5Dot1:
return fillPositions(channelPositions5Dot1);
case ChannelConfig::ChannelConfigSurround7Dot0:
return fillPositions(channelPositions7Dot0);
case ChannelConfig::ChannelConfigSurround7Dot1:
return fillPositions(channelPositions7Dot1);
case ChannelConfig::ChannelConfigUnknown:
default: {
#if !PW_CHECK_VERSION(0, 3, 33)
uint32_t SPA_AUDIO_CHANNEL_START_Aux = 0x1000;
#endif
// now we're in speculative territory: ChannelConfig is a bitmask and isn't
// able to represent arbitrary channel configurations.
//
// as a "best effort", we can try to populate all channels as "Aux" channels
// depending on the channel count
std::iota(info.position, info.position + fmt.channelCount(),
uint32_t(SPA_AUDIO_CHANNEL_START_Aux));
return;
}
}
}
} // namespace
spa_audio_info_raw asSpaAudioInfoRaw(const QAudioFormat &fmt)
{
spa_audio_info_raw ret{
.format = toSpaAudioFormat(fmt.sampleFormat()),
.flags = {},
.rate = uint32_t(fmt.sampleRate()),
.channels = uint32_t(fmt.channelCount()),
.position = {},
};
initializeChannelPositions(ret, fmt);
return ret;
}
} // namespace QtPipeWire
QT_END_NAMESPACE