summaryrefslogtreecommitdiffstats
path: root/src/spatialaudio/qambisonicdecoder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/spatialaudio/qambisonicdecoder.cpp')
-rw-r--r--src/spatialaudio/qambisonicdecoder.cpp314
1 files changed, 314 insertions, 0 deletions
diff --git a/src/spatialaudio/qambisonicdecoder.cpp b/src/spatialaudio/qambisonicdecoder.cpp
new file mode 100644
index 000000000..4404b11b0
--- /dev/null
+++ b/src/spatialaudio/qambisonicdecoder.cpp
@@ -0,0 +1,314 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
+#include "qambisonicdecoder_p.h"
+
+#include "qambisonicdecoderdata_p.h"
+#include <cmath>
+#include <qdebug.h>
+
+QT_BEGIN_NAMESPACE
+
+// Ambisonic decoding is described in detail in https://ambisonics.dreamhosters.com/BLaH3.pdf.
+// We're using a phase matched band splitting filter to split the ambisonic signal into a low
+// and high frequency component and apply matrix conversions to those components individually
+// as described in the document.
+//
+// We are currently not using a near field compensation filter, something that could potentially
+// improve sound quality further.
+//
+// For mono and stereo decoding, we use a simpler algorithm to avoid artificially dampening signals
+// coming from the back, as we do not have any speakers in that direction and the calculations
+// through matlab would give us audible 'holes'.
+
+struct QAmbisonicDecoderData
+{
+ QAudioFormat::ChannelConfig config;
+ const float *lf[3];
+ const float *hf[3];
+ const float *reverb;
+};
+
+const float reverb_x_0[] =
+{
+ 1.f, 0.f, // L
+ 0.f, 1.f, // R
+ .7f, .7f, // C
+ 1.f, 0.f, // Ls
+ 0.f, 1.f, // Rs
+ 1.f, 0.f, // Lb
+ 0.f, 1.f, // Rb
+};
+
+const float reverb_x_1[] =
+{
+ 1.f, 0.f, // L
+ 0.f, 1.f, // R
+ .7f, .7f, // C
+ .0f, .0f, // LFE
+ 1.f, 0.f, // Ls
+ 0.f, 1.f, // Rs
+ 1.f, 0.f, // Lb
+ 0.f, 1.f, // Rb
+};
+
+static const QAmbisonicDecoderData decoderMap[] =
+{
+ { QAudioFormat::ChannelConfigSurround5Dot0,
+ { decoderMatrix_5dot0_1_lf, decoderMatrix_5dot0_2_lf, decoderMatrix_5dot0_3_lf },
+ { decoderMatrix_5dot0_1_hf, decoderMatrix_5dot0_2_hf, decoderMatrix_5dot0_3_hf },
+ reverb_x_0
+ },
+ { QAudioFormat::ChannelConfigSurround5Dot1,
+ { decoderMatrix_5dot1_1_lf, decoderMatrix_5dot1_2_lf, decoderMatrix_5dot1_3_lf },
+ { decoderMatrix_5dot1_1_hf, decoderMatrix_5dot1_2_hf, decoderMatrix_5dot1_3_hf },
+ reverb_x_1
+ },
+ { QAudioFormat::ChannelConfigSurround7Dot0,
+ { decoderMatrix_7dot0_1_lf, decoderMatrix_7dot0_2_lf, decoderMatrix_7dot0_3_lf },
+ { decoderMatrix_7dot0_1_hf, decoderMatrix_7dot0_2_hf, decoderMatrix_7dot0_3_hf },
+ reverb_x_0
+ },
+ { QAudioFormat::ChannelConfigSurround7Dot1,
+ { decoderMatrix_7dot1_1_lf, decoderMatrix_7dot1_2_lf, decoderMatrix_7dot1_3_lf },
+ { decoderMatrix_7dot1_1_hf, decoderMatrix_7dot1_2_hf, decoderMatrix_7dot1_3_hf },
+ reverb_x_1
+ }
+};
+
+// Implements a split second order IIR filter
+// The audio data is split into a phase synced low and high frequency part
+// This allows us to apply different factors to both parts for better sound
+// localization when converting from ambisonic formats
+//
+// Details are described in https://ambisonics.dreamhosters.com/BLaH3.pdf, Appendix A.2.
+class QAmbisonicDecoderFilter
+{
+public:
+ QAmbisonicDecoderFilter() = default;
+ void configure(float sampleRate, float cutoffFrequency = 380)
+ {
+ double k = tan(M_PI*cutoffFrequency/sampleRate);
+ a1 = float(2.*(k*k - 1.)/(k*k + 2*k + 1.));
+ a2 = float((k*k - 2*k + 1.)/(k*k + 2*k + 1.));
+
+ b0_lf = float(k*k/(k*k + 2*k + 1));
+ b1_lf = 2.f*b0_lf;
+
+ b0_hf = float(1./(k*k + 2*k + 1));
+ b1_hf = -2.f*b0_hf;
+ }
+
+ struct Output
+ {
+ float lf;
+ float hf;
+ };
+
+ Output next(float x)
+ {
+ float r_lf = x*b0_lf +
+ prevX[0]*b1_lf +
+ prevX[1]*b0_lf -
+ prevR_lf[0]*a1 -
+ prevR_lf[1]*a2;
+ float r_hf = x*b0_hf +
+ prevX[0]*b1_hf +
+ prevX[1]*b0_hf -
+ prevR_hf[0]*a1 -
+ prevR_hf[1]*a2;
+ prevX[1] = prevX[0];
+ prevX[0] = x;
+ prevR_lf[1] = prevR_lf[0];
+ prevR_lf[0] = r_lf;
+ prevR_hf[1] = prevR_hf[0];
+ prevR_hf[0] = r_hf;
+ return { r_lf, r_hf };
+ }
+
+private:
+ float a1 = 0.;
+ float a2 = 0.;
+
+ float b0_hf = 0.;
+ float b1_hf = 0.;
+
+ float b0_lf = 0.;
+ float b1_lf = 0.;
+
+ float prevX[2] = {};
+ float prevR_lf[2] = {};
+ float prevR_hf[2] = {};
+};
+
+
+QAmbisonicDecoder::QAmbisonicDecoder(AmbisonicLevel ambisonicLevel, const QAudioFormat &format)
+ : level(ambisonicLevel)
+{
+ Q_ASSERT(level > 0 && level <= 3);
+ inputChannels = (level+1)*(level+1);
+ outputChannels = format.channelCount();
+
+ channelConfig = format.channelConfig();
+ if (channelConfig == QAudioFormat::ChannelConfigUnknown)
+ channelConfig = format.defaultChannelConfigForChannelCount(format.channelCount());
+
+ if (channelConfig == QAudioFormat::ChannelConfigMono ||
+ channelConfig == QAudioFormat::ChannelConfigStereo ||
+ channelConfig == QAudioFormat::ChannelConfig2Dot1 ||
+ channelConfig == QAudioFormat::ChannelConfig3Dot0 ||
+ channelConfig == QAudioFormat::ChannelConfig3Dot1) {
+ // these are non surround configs and handled manually to avoid
+ // audible holes for sounds coming from behing
+ //
+ // We use a simpler decoding process here, only taking first order
+ // ambisonics into account
+ //
+ // Left and right channels get 50% W and 50% X
+ // Center gets 50% W and 50% Y
+ // LFE gets 50% W
+ simpleDecoderFactors = new float[4*outputChannels];
+ float *r = new float[2*outputChannels]; // reverb output is in stereo
+ float *f = simpleDecoderFactors;
+ reverbFactors = r;
+ if (channelConfig & QAudioFormat::channelConfig(QAudioFormat::FrontLeft)) {
+ f[0] = 0.5f; f[1] = 0.5f; f[2] = 0.; f[3] = 0.f;
+ f += 4;
+ r[0] = 1.; r[1] = 0.;
+ r += 2;
+ }
+ if (channelConfig & QAudioFormat::channelConfig(QAudioFormat::FrontRight)) {
+ f[0] = 0.5f; f[1] = -0.5f; f[2] = 0.; f[3] = 0.f;
+ f += 4;
+ r[0] = 0.; r[1] = 1.;
+ r += 2;
+ }
+ if (channelConfig & QAudioFormat::channelConfig(QAudioFormat::FrontCenter)) {
+ f[0] = 0.5f; f[1] = -0.f; f[2] = 0.; f[3] = 0.5f;
+ f += 4;
+ r[0] = .5; r[1] = .5;
+ r += 2;
+ }
+ if (channelConfig & QAudioFormat::channelConfig(QAudioFormat::LFE)) {
+ f[0] = 0.5f; f[1] = -0.f; f[2] = 0.; f[3] = 0.0f;
+ f += 4;
+ r[0] = 0.; r[1] = 0.;
+ r += 2;
+ }
+ Q_UNUSED(f);
+ Q_UNUSED(r);
+ Q_ASSERT((f - simpleDecoderFactors) == 4*outputChannels);
+ Q_ASSERT((r - reverbFactors) == 2*outputChannels);
+
+ return;
+ }
+
+ for (const auto &d : decoderMap) {
+ if (d.config == channelConfig) {
+ decoderData = &d;
+ reverbFactors = decoderData->reverb;
+ break;
+ }
+ }
+ if (!decoderData) {
+ // can't handle this,
+ outputChannels = 0;
+ return;
+ }
+
+ filters = new QAmbisonicDecoderFilter[inputChannels];
+ for (int i = 0; i < inputChannels; ++i)
+ filters[i].configure(format.sampleRate());
+}
+
+QAmbisonicDecoder::~QAmbisonicDecoder()
+{
+ if (simpleDecoderFactors) {
+ delete simpleDecoderFactors;
+ delete reverbFactors;
+ }
+}
+
+void QAmbisonicDecoder::processBuffer(const float *input[], float *output, int nSamples)
+{
+ float *o = output;
+ memset(o, 0, nSamples*outputChannels*sizeof(float));
+
+ if (simpleDecoderFactors) {
+ for (int i = 0; i < nSamples; ++i) {
+ for (int j = 0; j < 4; ++j) {
+ for (int k = 0; k < outputChannels; ++k)
+ o[k] += simpleDecoderFactors[k*4 + j]*input[j][i];
+ }
+ o += outputChannels;
+ }
+ return;
+ }
+
+ const float *matrix_hi = decoderData->hf[level - 1];
+ const float *matrix_lo = decoderData->lf[level - 1];
+ for (int i = 0; i < nSamples; ++i) {
+ QAmbisonicDecoderFilter::Output buf[maxAmbisonicChannels];
+ for (int j = 0; j < inputChannels; ++j)
+ buf[j] = filters[j].next(input[j][i]);
+ for (int j = 0; j < inputChannels; ++j) {
+ for (int k = 0; k < outputChannels; ++k)
+ o[k] += matrix_lo[k*inputChannels + j]*buf[j].lf + matrix_hi[k*inputChannels + j]*buf[j].hf;
+ }
+ o += outputChannels;
+ }
+}
+
+void QAmbisonicDecoder::processBuffer(const float *input[], short *output, int nSamples)
+{
+ const float *reverb[] = { nullptr, nullptr };
+ return processBufferWithReverb(input, reverb, output, nSamples);
+}
+
+void QAmbisonicDecoder::processBufferWithReverb(const float *input[], const float *reverb[2], short *output, int nSamples)
+{
+ if (simpleDecoderFactors) {
+ for (int i = 0; i < nSamples; ++i) {
+ float o[4] = {};
+ for (int k = 0; k < outputChannels; ++k) {
+ for (int j = 0; j < 4; ++j)
+ o[k] += simpleDecoderFactors[k*4 + j]*input[j][i];
+ }
+ if (reverb[0]) {
+ for (int k = 0; k < outputChannels; ++k) {
+ o[k] += reverb[0][i]*reverbFactors[2*k] + reverb[1][i]*reverbFactors[2*k+1];
+ }
+ }
+
+ for (int k = 0; k < outputChannels; ++k)
+ output[k] = static_cast<short>(o[k]*32768.);
+ output += outputChannels;
+ }
+ return;
+ }
+
+ // qDebug() << "XXX" << inputChannels << outputChannels;
+ const float *matrix_hi = decoderData->hf[level - 1];
+ const float *matrix_lo = decoderData->lf[level - 1];
+ for (int i = 0; i < nSamples; ++i) {
+ QAmbisonicDecoderFilter::Output buf[maxAmbisonicChannels];
+ for (int j = 0; j < inputChannels; ++j)
+ buf[j] = filters[j].next(input[j][i]);
+ float o[32] = {}; // we can't support more than 32 channels from our API
+ for (int j = 0; j < inputChannels; ++j) {
+ for (int k = 0; k < outputChannels; ++k)
+ o[k] += matrix_lo[k*inputChannels + j]*buf[j].lf + matrix_hi[k*inputChannels + j]*buf[j].hf;
+ }
+ if (reverb[0]) {
+ for (int k = 0; k < outputChannels; ++k) {
+ o[k] += reverb[0][i]*reverbFactors[2*k] + reverb[1][i]*reverbFactors[2*k+1];
+ }
+ }
+ for (int k = 0; k < outputChannels; ++k)
+ output[k] = static_cast<short>(o[k]*32768.);
+ output += outputChannels;
+ }
+
+}
+
+QT_END_NAMESPACE
+