Revision control
Copy as Markdown
// Copyright (C) 2021 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 "qaudiohelpers_p.h"
#include <QtMultimedia/private/qaudio_qspan_support_p.h>
#include <QtMultimedia/private/qmultimedia_ranges_p.h>
#include <QtMultimedia/private/qmultimedia_enum_to_string_converter_p.h>
#include <QtCore/qdebug.h>
#include <algorithm>
#include <limits>
QT_BEGIN_NAMESPACE
namespace QAudioHelperInternal
{
#if defined(Q_CC_GNU)
# define QT_MM_RESTRICT __restrict__
#elif defined(Q_CC_MSVC)
# define QT_MM_RESTRICT __restrict
#else
# define QT_MM_RESTRICT /*__restrict__*/
#endif
namespace {
template<class T>
inline T applyVolumeOnSample(T sample, float factor)
{
if constexpr (std::is_signed_v<T>) {
return sample * factor;
} else {
using SignedT = std::make_signed_t<T>;
// Unsigned samples are biased around 0x80/0x8000
constexpr T offset = SignedT(1) << (std::numeric_limits<T>::digits - 1);
return offset + (SignedT(sample - offset) * factor);
}
}
template<class T>
void adjustSamples(float factor,
const void *QT_MM_RESTRICT src,
void *QT_MM_RESTRICT dst,
int samples)
{
const T *pSrc = (const T *)src;
T *pDst = (T *)dst;
for (int i = 0; i < samples; i++)
pDst[i] = applyVolumeOnSample(pSrc[i], factor);
}
} // namespace
void qMultiplySamples(float factor,
const QAudioFormat &format,
const void *src,
void *dest,
int len) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
const int samplesCount = len / qMax(1, format.bytesPerSample());
auto clamp = [](float arg) {
float realVolume = std::clamp<float>(arg, 0.f, 1.f);
return realVolume;
};
switch (format.sampleFormat()) {
case QAudioFormat::UInt8:
return QAudioHelperInternal::adjustSamples<quint8>(clamp(factor), src, dest, samplesCount);
case QAudioFormat::Int16:
return QAudioHelperInternal::adjustSamples<qint16>(clamp(factor), src, dest, samplesCount);
case QAudioFormat::Int32:
return QAudioHelperInternal::adjustSamples<qint32>(clamp(factor), src, dest, samplesCount);
case QAudioFormat::Float:
return QAudioHelperInternal::adjustSamples<float>(factor, src, dest, samplesCount);
default:
Q_UNREACHABLE_RETURN();
}
}
void applyVolume(float volume,
const QAudioFormat &format,
QSpan<const std::byte> source,
QSpan<std::byte> destination) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
Q_ASSERT(source.size() == destination.size());
if (Q_LIKELY(volume == 1.f)) {
if (source.data() != destination.data())
std::copy(source.begin(), source.end(), destination.begin());
} else if (volume == 0) {
std::byte zero =
format.sampleFormat() == QAudioFormat::UInt8 ? std::byte{ 0x80 } : std::byte{ 0 };
std::fill(destination.begin(), destination.begin() + source.size(), zero);
} else {
QAudioHelperInternal::qMultiplySamples(volume, format, source.data(), destination.data(),
source.size());
}
}
namespace {
template <NativeSampleFormat Format>
Q_ALWAYS_INLINE constexpr int32_t toInt32(QSpan<const std::byte> source) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
switch (Format) {
case NativeSampleFormat::uint8_t: {
constexpr int32_t offset = (1 << 7);
uint8_t smp = uint8_t(source.front());
int32_t val = (int32_t(smp) - offset) << 24;
return val;
}
case NativeSampleFormat::int16_t: {
union CastUnion {
std::int16_t i16;
std::byte b[2];
} castUnion = {};
std::copy_n(source.data(), 2, castUnion.b);
int32_t val = castUnion.i16 << 16;
return val;
}
case NativeSampleFormat::int24_t_3b: {
union CastUnion {
std::int32_t i32;
std::byte b[4];
} castUnion = {};
std::copy_n(source.data(), 3, castUnion.b);
int32_t val = castUnion.i32 << 8;
return val;
}
case NativeSampleFormat::int24_t_4b_low: {
union CastUnion {
std::int32_t i32;
std::byte b[4];
} castUnion = {};
std::copy_n(source.data(), 4, castUnion.b);
int32_t val = castUnion.i32 << 8;
return val;
}
case NativeSampleFormat::float32_t: {
union CastUnion {
float f32;
std::byte b[4];
} castUnion = {};
std::copy_n(source.data(), 4, castUnion.b);
constexpr double range = std::numeric_limits<int32_t>::max();
int32_t val = (castUnion.f32 * range);
return val;
}
case NativeSampleFormat::int32_t: {
union CastUnion {
std::int32_t i32;
std::byte b[4];
} castUnion = {};
std::copy_n(source.data(), 4, castUnion.b);
return castUnion.i32;
}
default:
Q_UNREACHABLE_RETURN({});
}
}
template <NativeSampleFormat Format>
Q_ALWAYS_INLINE constexpr void storeSampleWithFormat(QSpan<std::byte> destination,
int32_t value) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
switch (Format) {
case NativeSampleFormat::uint8_t: {
value = value >> 24;
constexpr int32_t offset = 1 << 7;
uint8_t sampleValue = value + offset;
std::copy_n(reinterpret_cast<const std::byte *>(&sampleValue), 1, destination.data());
return;
}
case NativeSampleFormat::int16_t: {
value = value >> 16;
int16_t sampleValue = value;
std::copy_n(reinterpret_cast<const std::byte *>(&sampleValue), 2, destination.data());
return;
}
case NativeSampleFormat::int24_t_3b: {
int32_t sampleValue = value >> 8;
std::copy_n(reinterpret_cast<const std::byte *>(&sampleValue), 3, destination.data());
return;
}
case NativeSampleFormat::int24_t_4b_low: {
int32_t sampleValue = (value >> 8) & 0x00ffffff;
std::copy_n(reinterpret_cast<const std::byte *>(&sampleValue), 4, destination.data());
return;
}
case NativeSampleFormat::int32_t: {
std::copy_n(reinterpret_cast<const std::byte *>(&value), 4, destination.data());
return;
}
case NativeSampleFormat::float32_t: {
constexpr double normalizationFactor = 1.0 / std::numeric_limits<int32_t>::max();
float sampleValue = float(double(value) * normalizationFactor);
std::copy_n(reinterpret_cast<const std::byte *>(&sampleValue), 4, destination.data());
return;
}
default:
Q_UNREACHABLE_RETURN();
}
}
template <NativeSampleFormat sourceFormat, NativeSampleFormat destinationFormat>
struct WordConverter
{
void operator()(QSpan<const std::byte> source, QSpan<std::byte> destination)
{
if constexpr (sourceFormat == destinationFormat) {
std::copy(source.begin(), source.end(), destination.begin());
} else {
constexpr size_t bytesPerSampleSource = bytesPerSample(sourceFormat);
constexpr size_t bytesPerSampleDest = bytesPerSample(destinationFormat);
using namespace QtMultimediaPrivate;
while (!source.isEmpty()) {
if (size_t(source.size()) >= 4 * bytesPerSampleSource) {
// unroll by 4.
// should help the compiler to vectorize, or at least avoid pipeline stalls.
auto sourceSampleRange1 = take(source, bytesPerSampleSource);
source = drop(source, bytesPerSampleSource);
auto sourceSampleRange2 = take(source, bytesPerSampleSource);
source = drop(source, bytesPerSampleSource);
auto sourceSampleRange3 = take(source, bytesPerSampleSource);
source = drop(source, bytesPerSampleSource);
auto sourceSampleRange4 = take(source, bytesPerSampleSource);
source = drop(source, bytesPerSampleSource);
int32_t value1 = toInt32<sourceFormat>(sourceSampleRange1);
int32_t value2 = toInt32<sourceFormat>(sourceSampleRange2);
int32_t value3 = toInt32<sourceFormat>(sourceSampleRange3);
int32_t value4 = toInt32<sourceFormat>(sourceSampleRange4);
auto destSampleRange1 = take(destination, bytesPerSampleDest);
destination = drop(destination, bytesPerSampleDest);
auto destSampleRange2 = take(destination, bytesPerSampleDest);
destination = drop(destination, bytesPerSampleDest);
auto destSampleRange3 = take(destination, bytesPerSampleDest);
destination = drop(destination, bytesPerSampleDest);
auto destSampleRange4 = take(destination, bytesPerSampleDest);
destination = drop(destination, bytesPerSampleDest);
storeSampleWithFormat<destinationFormat>(destSampleRange1, value1);
storeSampleWithFormat<destinationFormat>(destSampleRange2, value2);
storeSampleWithFormat<destinationFormat>(destSampleRange3, value3);
storeSampleWithFormat<destinationFormat>(destSampleRange4, value4);
} else {
auto sourceSampleRange = take(source, bytesPerSampleSource);
int32_t value = toInt32<sourceFormat>(sourceSampleRange);
auto destSampleRange = take(destination, bytesPerSampleDest);
storeSampleWithFormat<destinationFormat>(destSampleRange, value);
source = drop(source, bytesPerSampleSource);
destination = drop(destination, bytesPerSampleDest);
}
}
}
}
};
template <NativeSampleFormat destinationFormat>
void convertSampleFormatWithDestinationFormat(QSpan<const std::byte> source,
NativeSampleFormat sourceFormat,
QSpan<std::byte> destination) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
switch (sourceFormat) {
case NativeSampleFormat::uint8_t:
return WordConverter<NativeSampleFormat::uint8_t, destinationFormat>()(source, destination);
case NativeSampleFormat::int16_t:
return WordConverter<NativeSampleFormat::int16_t, destinationFormat>()(source, destination);
case NativeSampleFormat::int32_t:
return WordConverter<NativeSampleFormat::int32_t, destinationFormat>()(source, destination);
case NativeSampleFormat::int24_t_3b:
return WordConverter<NativeSampleFormat::int24_t_3b, destinationFormat>()(source,
destination);
case NativeSampleFormat::int24_t_4b_low:
return WordConverter<NativeSampleFormat::int24_t_4b_low, destinationFormat>()(source,
destination);
case NativeSampleFormat::float32_t:
return WordConverter<NativeSampleFormat::float32_t, destinationFormat>()(source,
destination);
default:
Q_UNREACHABLE_RETURN();
}
}
} // namespace
void convertSampleFormat(QSpan<const std::byte> source, NativeSampleFormat sourceFormat,
QSpan<std::byte> destination,
NativeSampleFormat destinationFormat) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
using namespace QtMultimediaPrivate;
size_t bytesPerSampleSource = bytesPerSample(sourceFormat);
size_t bytesPerSampleDest = bytesPerSample(destinationFormat);
Q_ASSERT(source.size() * bytesPerSampleDest == destination.size() * bytesPerSampleSource);
switch (destinationFormat) {
case NativeSampleFormat::uint8_t:
return convertSampleFormatWithDestinationFormat<NativeSampleFormat::uint8_t>(
source, sourceFormat, destination);
case NativeSampleFormat::int16_t:
return convertSampleFormatWithDestinationFormat<NativeSampleFormat::int16_t>(
source, sourceFormat, destination);
case NativeSampleFormat::int32_t:
return convertSampleFormatWithDestinationFormat<NativeSampleFormat::int32_t>(
source, sourceFormat, destination);
case NativeSampleFormat::int24_t_3b:
return convertSampleFormatWithDestinationFormat<NativeSampleFormat::int24_t_3b>(
source, sourceFormat, destination);
case NativeSampleFormat::int24_t_4b_low:
return convertSampleFormatWithDestinationFormat<NativeSampleFormat::int24_t_4b_low>(
source, sourceFormat, destination);
case NativeSampleFormat::float32_t:
return convertSampleFormatWithDestinationFormat<NativeSampleFormat::float32_t>(
source, sourceFormat, destination);
default:
Q_UNREACHABLE_RETURN();
}
}
NativeSampleFormat bestNativeSampleFormat(const QAudioFormat &fmt,
QSpan<const NativeSampleFormat> supportedNativeFormats)
{
Q_ASSERT(!supportedNativeFormats.empty());
auto resolveCandidate = [&](QSpan<const NativeSampleFormat> candidates) {
auto it = std::find_first_of(candidates.begin(), candidates.end(),
supportedNativeFormats.begin(), supportedNativeFormats.end());
if (it != candidates.end())
return *it;
qFatal() << "No candidate for conversion found. That should not happen";
Q_UNREACHABLE_RETURN(NativeSampleFormat::float32_t);
};
// heuristics:
// select the best format that does not involve precision
// if that does not yield a format, find a format with the least loss of precision.
switch (fmt.sampleFormat()) {
case QAudioFormat::SampleFormat::UInt8: {
constexpr auto candidates = std::array{
NativeSampleFormat::uint8_t, NativeSampleFormat::int16_t,
NativeSampleFormat::int24_t_3b, NativeSampleFormat::int24_t_4b_low,
NativeSampleFormat::int32_t, NativeSampleFormat::float32_t,
};
return resolveCandidate(candidates);
}
case QAudioFormat::SampleFormat::Int16: {
constexpr auto candidates = std::array{
NativeSampleFormat::int16_t, NativeSampleFormat::int24_t_3b,
NativeSampleFormat::int24_t_4b_low, NativeSampleFormat::int32_t,
NativeSampleFormat::float32_t, NativeSampleFormat::uint8_t,
};
return resolveCandidate(candidates);
}
case QAudioFormat::SampleFormat::Int32: {
constexpr auto candidates = std::array{
NativeSampleFormat::int32_t, NativeSampleFormat::float32_t,
NativeSampleFormat::int24_t_3b, NativeSampleFormat::int24_t_4b_low,
NativeSampleFormat::int16_t, NativeSampleFormat::uint8_t,
};
return resolveCandidate(candidates);
}
case QAudioFormat::SampleFormat::Float: {
constexpr auto candidates = std::array{
NativeSampleFormat::float32_t, NativeSampleFormat::int24_t_3b,
NativeSampleFormat::int24_t_4b_low, NativeSampleFormat::int32_t,
NativeSampleFormat::int16_t, NativeSampleFormat::uint8_t,
};
return resolveCandidate(candidates);
}
case QAudioFormat::SampleFormat::Unknown:
case QAudioFormat::SampleFormat::NSampleFormats: {
qFatal() << "bestNativeSampleFormat called with invalid argument" << fmt.sampleFormat();
Q_UNREACHABLE_RETURN(NativeSampleFormat::int32_t);
}
default: {
Q_UNREACHABLE_RETURN(NativeSampleFormat::int32_t);
}
}
}
NativeSampleFormat toNativeSampleFormat(QAudioFormat::SampleFormat fmt)
{
switch (fmt) {
case QAudioFormat::SampleFormat::Float:
return NativeSampleFormat::float32_t;
case QAudioFormat::SampleFormat::UInt8:
return NativeSampleFormat::uint8_t;
case QAudioFormat::SampleFormat::Int16:
return NativeSampleFormat::int16_t;
case QAudioFormat::SampleFormat::Int32:
return NativeSampleFormat::int32_t;
default:
Q_UNREACHABLE_RETURN({});
}
}
QAudioFormat::SampleFormat bestSampleFormat(const NativeSampleFormat fmt)
{
switch (fmt) {
case NativeSampleFormat::uint8_t:
return QAudioFormat::SampleFormat::UInt8;
case NativeSampleFormat::int16_t:
return QAudioFormat::SampleFormat::Int16;
case NativeSampleFormat::int32_t:
case NativeSampleFormat::int24_t_3b:
case NativeSampleFormat::int24_t_4b_low:
return QAudioFormat::SampleFormat::Int32;
case NativeSampleFormat::float32_t:
return QAudioFormat::SampleFormat::Float;
default:
Q_UNREACHABLE_RETURN({});
}
}
std::optional<float> sanitizeVolume(float volume, float lastValue)
{
constexpr float epsilon = 1.f / (1 << 22); // good enough for 22bit resolution
// multiplication is optimized for 0 and 1
// values which are sufficiently close to 0 and 1 can be considered as 0 or 1
if (volume < epsilon)
volume = 0;
else if (volume > (1.f - epsilon))
volume = 1.f;
// similar to qFuzzyCompare, but with better heuristics for volume
if (std::abs(volume - lastValue) < epsilon)
return std::nullopt;
return volume;
}
void fillSilence(QSpan<std::byte> buffer,
NativeSampleFormat sampleFormat) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
namespace ranges = QtMultimediaPrivate::ranges;
ranges::fill(buffer,
sampleFormat == NativeSampleFormat::uint8_t ? std::byte{ 0x80 } : std::byte{ 0 });
}
void fillSilence(QSpan<std::byte> buffer, QAudioFormat fmt) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
fillSilence(buffer, toNativeSampleFormat(fmt.sampleFormat()));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace {
struct CatmullRomBasis
{
explicit CatmullRomBasis(float t)
{
Q_ASSERT(t >= 0.0f && t < 1.0f);
const float t2 = t * t;
const float t3 = t2 * t;
c0 = -0.5f * t3 + t2 - 0.5f * t;
c1 = 1.5f * t3 - 2.5f * t2 + 1.0f;
c2 = -1.5f * t3 + 2.0f * t2 + 0.5f * t;
c3 = 0.5f * t3 - 0.5f * t2;
}
float interpolate(float pm1, float p0, float p1, float p2) const
{
return (c0 * pm1 + c1 * p0) + (c2 * p1 + c3 * p2);
}
private:
float c0, c1, c2, c3;
};
} // namespace
QByteArray resampleAudioCatmullRom(QSpan<const float> input, int nChannels, int inputRate,
int outputRate)
{
Q_ASSERT(nChannels >= 1);
Q_ASSERT(inputRate > 0);
Q_ASSERT(outputRate > 0);
if (input.isEmpty() || nChannels <= 0)
return {};
if (inputRate == outputRate) {
return QByteArray(reinterpret_cast<const char *>(input.data()),
qsizetype(input.size()) * qsizetype(sizeof(float)));
}
const qsizetype inputFrames = input.size() / nChannels;
if (inputFrames == 0)
return {};
const double resampleRatio = double(outputRate) / double(inputRate);
// Number of output frames
const qsizetype outputFrames = std::lround(inputFrames * resampleRatio);
if (outputFrames <= 0)
return {};
QByteArray out;
out.resizeForOverwrite(outputFrames * nChannels * qsizetype(sizeof(float)));
QSpan<float> dst(reinterpret_cast<float *>(out.data()), outputFrames * nChannels);
using namespace QtMultimediaPrivate;
// Catmull-Rom: needs p0,p1,p2,p3 where output samples between p1 and p2
// p[i] = clamp(i, 0, inputFrames-1)
const float increment = 1.f / float(resampleRatio);
qsizetype iPos = 0;
float fPos = 0.0f;
for (qsizetype outFrame = 0; outFrame < outputFrames; ++outFrame) {
const qsizetype i1 = iPos;
const float t = fPos;
// Clamp frame indices
const qsizetype i0 = std::max(qsizetype(0), i1 - 1);
const qsizetype i2 = std::min(i1 + 1, inputFrames - 1);
const qsizetype i3 = std::min(i1 + 2, inputFrames - 1);
const CatmullRomBasis basis(t);
auto frameOut = take(dst, nChannels);
for (int ch = 0; ch < nChannels; ++ch) {
const float pm1 = input[i0 * nChannels + ch];
const float p0 = input[i1 * nChannels + ch];
const float p1 = input[i2 * nChannels + ch];
const float p2 = input[i3 * nChannels + ch];
frameOut[ch] = basis.interpolate(pm1, p0, p1, p2);
}
dst = drop(dst, nChannels);
fPos += increment;
while (fPos >= 1.0f) {
fPos -= 1.0f;
iPos += 1;
}
}
return out;
}
CatmullRomInterpolator::CatmullRomInterpolator(int nChannels, int inputRate, int outputRate)
: m_nChannels(nChannels), m_ratio(float(inputRate) / float(outputRate))
{
Q_ASSERT(nChannels >= 1);
Q_ASSERT(inputRate > 0);
Q_ASSERT(outputRate > 0);
m_history.assign(3 * nChannels, 0.0f);
}
void CatmullRomInterpolator::reset()
{
m_iPos = 0;
m_fPos = 0.0f;
m_inputFramesConsumed = 0;
namespace ranges = QtMultimediaPrivate::ranges;
ranges::fill(m_history, 0.0f);
}
void CatmullRomInterpolator::advancePosition()
{
m_fPos += m_ratio;
while (m_fPos >= 1.0f) {
m_fPos -= 1.0f;
m_iPos += 1;
}
}
auto CatmullRomInterpolator::process(QSpan<const float> input,
QSpan<float> output) noexcept Q_DECL_NONBLOCKING_FUNCTION
-> ResampleResult
{
using namespace QtMultimediaPrivate;
Q_PRESUME(m_nChannels >= 1);
const qsizetype nChannels = m_nChannels;
const qsizetype inputFrames = input.size() / nChannels;
const qsizetype maxOutputFrames = output.size() / nChannels;
// Flush mode: empty input signals end-of-stream
if (inputFrames == 0) {
qsizetype outputFramePos = 0;
const qsizetype lastAvailableFrame = m_inputFramesConsumed - 1;
while (outputFramePos < maxOutputFrames) {
if (m_iPos > lastAvailableFrame)
break;
const qsizetype pm1FrameIdx = std::max(qsizetype(0), m_iPos - 1);
const qsizetype p1FrameIdx = std::min(m_iPos + 1, lastAvailableFrame);
const qsizetype p2FrameIdx = std::min(m_iPos + 2, lastAvailableFrame);
const CatmullRomBasis basis(m_fPos);
for (qsizetype ch = 0; ch < nChannels; ++ch) {
auto fetch = [&](qsizetype absoluteFrameIdx) -> float {
if (absoluteFrameIdx < m_inputFramesConsumed) {
const qsizetype historySampleIdx =
absoluteFrameIdx - (m_inputFramesConsumed - 3);
if (historySampleIdx >= 0)
return m_history[historySampleIdx * nChannels + ch];
return m_history[ch]; // clamp to oldest history
}
return m_history[ch]; // beyond available data, clamp
};
const float pm1 = fetch(pm1FrameIdx);
const float p0 = fetch(m_iPos);
const float p1 = fetch(p1FrameIdx);
const float p2 = fetch(p2FrameIdx);
output[outputFramePos * nChannels + ch] =
basis.interpolate(pm1, p0, p1, p2);
}
advancePosition();
++outputFramePos;
}
return ResampleResult{
input,
take(output, outputFramePos * nChannels),
};
}
qsizetype outputFramePos = 0;
qsizetype maxConsumedFrames = 0;
while (outputFramePos < maxOutputFrames) {
if (m_iPos + 2 >= m_inputFramesConsumed + inputFrames)
break;
// Clamp frame indices relative to available data
const qsizetype pm1FrameIdx = std::max(qsizetype(0), m_iPos - 1);
const qsizetype p1FrameIdx =
std::min(m_iPos + 1, m_inputFramesConsumed + inputFrames - 1);
const qsizetype p2FrameIdx =
std::min(m_iPos + 2, m_inputFramesConsumed + inputFrames - 1);
const CatmullRomBasis basis(m_fPos);
for (qsizetype ch = 0; ch < nChannels; ++ch) {
auto fetch = [&](qsizetype absoluteFrameIdx) -> float {
if (absoluteFrameIdx >= m_inputFramesConsumed)
return input[(absoluteFrameIdx - m_inputFramesConsumed) * nChannels + ch];
const qsizetype historySampleIdx =
absoluteFrameIdx - (m_inputFramesConsumed - 3);
if (historySampleIdx >= 0)
return m_history[historySampleIdx * nChannels + ch];
return m_history[ch]; // clamp to oldest history (stream start)
};
const float pm1 = fetch(pm1FrameIdx);
const float p0 = fetch(m_iPos);
const float p1 = fetch(p1FrameIdx);
const float p2 = fetch(p2FrameIdx);
output[outputFramePos * nChannels + ch] =
basis.interpolate(pm1, p0, p1, p2);
}
maxConsumedFrames =
std::max(maxConsumedFrames,
m_iPos + 3 - m_inputFramesConsumed);
advancePosition();
++outputFramePos;
}
// Update history with last 3 frames of consumed portion
qsizetype framesToCache = std::min(maxConsumedFrames, inputFrames);
// If the main loop produced no output but there is input (m_iPos lags
// behind m_inputFramesConsumed at a chunk boundary), consume all input into history so
// the caller makes progress. The flush path interpolates using history.
if (framesToCache == 0 && inputFrames > 0)
framesToCache = inputFrames;
namespace ranges = QtMultimediaPrivate::ranges;
if (framesToCache >= 3) {
const qsizetype last3FramesSampleOffset = (framesToCache - 3) * nChannels;
ranges::copy(take(drop(input, last3FramesSampleOffset), 3 * nChannels),
m_history.data());
} else if (framesToCache > 0) {
const qsizetype newCacheSampleCount = framesToCache * nChannels;
const qsizetype retainedSampleCount = (3 - framesToCache) * nChannels;
auto history = QSpan(m_history);
ranges::copy(take(drop(history, newCacheSampleCount), retainedSampleCount),
history.begin());
ranges::copy(take(input, newCacheSampleCount),
drop(history, retainedSampleCount).begin());
}
m_inputFramesConsumed += framesToCache;
const qsizetype unconsumedInputFrames = inputFrames - framesToCache;
return ResampleResult{
take(drop(input, framesToCache * nChannels), unconsumedInputFrames * nChannels),
take(output, outputFramePos * nChannels),
};
}
void upmixMonoToStereo(QSpan<float> out, QSpan<const float> in,
UpmixScaling scaling) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
constexpr float kEqualPowerGain(M_SQRT1_2); // 1/sqrt(2)
Q_ASSERT(out.size() == in.size() * 2);
const qsizetype frames = in.size();
const float gain = (scaling == UpmixScaling::EqualPower) ? kEqualPowerGain : 1.0f;
for (qsizetype i = 0; i < frames; ++i) {
const float s = in[i] * gain;
out[i * 2 + 0] = s;
out[i * 2 + 1] = s;
}
}
QByteArray upmixMonoToStereo(QSpan<const float> input, UpmixScaling scaling)
{
if (input.empty())
return {};
const qsizetype frames = input.size();
QByteArray out{
2 * frames * qsizetype(sizeof(float)),
Qt::Initialization::Uninitialized,
};
upmixMonoToStereo(QSpan<float>(reinterpret_cast<float *>(out.data()), 2 * frames),
input, scaling);
return out;
}
void downmixStereoToMono(QSpan<float> out, QSpan<const float> in,
DownmixScaling scaling) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
constexpr float kEqualPowerGain(M_SQRT1_2); // 1/sqrt(2)
Q_ASSERT(in.size() % 2 == 0);
Q_ASSERT(out.size() == in.size() / 2);
const qsizetype frames = in.size() / 2;
const float gain = (scaling == DownmixScaling::KeepPower) ? kEqualPowerGain : 0.5f;
for (qsizetype i = 0; i < frames; ++i)
out[i] = (in[i * 2 + 0] + in[i * 2 + 1]) * gain;
}
QByteArray downmixStereoToMono(QSpan<const float> input, DownmixScaling scaling)
{
if (input.empty())
return {};
Q_ASSERT(input.size() % 2 == 0);
const qsizetype frames = input.size() / 2;
QByteArray out{
frames * qsizetype(sizeof(float)),
Qt::Initialization::Uninitialized,
};
downmixStereoToMono(QSpan<float>(reinterpret_cast<float *>(out.data()), frames),
input, scaling);
return out;
}
} // namespace QAudioHelperInternal
#undef QT_MM_RESTRICT
// clang-format off
QT_MM_MAKE_STRING_RESOLVER( QAudioHelperInternal::NativeSampleFormat, QtMultimediaPrivate::EnumName,
(QAudioHelperInternal::NativeSampleFormat::uint8_t, "uint8_t")
(QAudioHelperInternal::NativeSampleFormat::int16_t, "int16_t")
(QAudioHelperInternal::NativeSampleFormat::int32_t, "int32_t")
(QAudioHelperInternal::NativeSampleFormat::int24_t_3b, "int24_t_3b")
(QAudioHelperInternal::NativeSampleFormat::int24_t_4b_low, "int24_t_4b_low")
(QAudioHelperInternal::NativeSampleFormat::float32_t, "float32_t")
);
// clang-format on
QT_MM_DEFINE_QDEBUG_ENUM(QAudioHelperInternal::NativeSampleFormat);
QT_END_NAMESPACE