AAudio: add a limiter instead of clipping audio
AAudio currently clips audio above sqrt(2) and below -sqrt(2).
Instead, for a smooth transition, a polynomial spline should be used.
Bug: 17914011
Test: atest test_flowgraph
Test: atest AAudioTests
Test: Test Output in OboeTester
Change-Id: Ia48537c9c914b71f6928adfc2470f17b3108e5d5
diff --git a/media/libaaudio/src/Android.bp b/media/libaaudio/src/Android.bp
index 363d219..1eba24d 100644
--- a/media/libaaudio/src/Android.bp
+++ b/media/libaaudio/src/Android.bp
@@ -209,6 +209,7 @@
"flowgraph/ChannelCountConverter.cpp",
"flowgraph/ClipToRange.cpp",
"flowgraph/FlowGraphNode.cpp",
+ "flowgraph/Limiter.cpp",
"flowgraph/ManyToMultiConverter.cpp",
"flowgraph/MonoBlend.cpp",
"flowgraph/MonoToMultiConverter.cpp",
diff --git a/media/libaaudio/src/client/AAudioFlowGraph.cpp b/media/libaaudio/src/client/AAudioFlowGraph.cpp
index 2ed3e3c..5444565 100644
--- a/media/libaaudio/src/client/AAudioFlowGraph.cpp
+++ b/media/libaaudio/src/client/AAudioFlowGraph.cpp
@@ -20,7 +20,7 @@
#include "AAudioFlowGraph.h"
-#include <flowgraph/ClipToRange.h>
+#include <flowgraph/Limiter.h>
#include <flowgraph/ManyToMultiConverter.h>
#include <flowgraph/MonoBlend.h>
#include <flowgraph/MonoToMultiConverter.h>
@@ -78,11 +78,11 @@
}
// For a pure float graph, there is chance that the data range may be very large.
- // So we should clip to a reasonable value that allows a little headroom.
+ // So we should limit to a reasonable value that allows a little headroom.
if (sourceFormat == AUDIO_FORMAT_PCM_FLOAT && sinkFormat == AUDIO_FORMAT_PCM_FLOAT) {
- mClipper = std::make_unique<ClipToRange>(sourceChannelCount);
- lastOutput->connect(&mClipper->input);
- lastOutput = &mClipper->output;
+ mLimiter = std::make_unique<Limiter>(sourceChannelCount);
+ lastOutput->connect(&mLimiter->input);
+ lastOutput = &mLimiter->output;
}
// Expand the number of channels if required.
diff --git a/media/libaaudio/src/client/AAudioFlowGraph.h b/media/libaaudio/src/client/AAudioFlowGraph.h
index 602c17f..35fef37 100644
--- a/media/libaaudio/src/client/AAudioFlowGraph.h
+++ b/media/libaaudio/src/client/AAudioFlowGraph.h
@@ -24,7 +24,7 @@
#include <aaudio/AAudio.h>
#include <audio_utils/Balance.h>
-#include <flowgraph/ClipToRange.h>
+#include <flowgraph/Limiter.h>
#include <flowgraph/ManyToMultiConverter.h>
#include <flowgraph/MonoBlend.h>
#include <flowgraph/MonoToMultiConverter.h>
@@ -74,7 +74,7 @@
private:
std::unique_ptr<FLOWGRAPH_OUTER_NAMESPACE::flowgraph::FlowGraphSourceBuffered> mSource;
std::unique_ptr<FLOWGRAPH_OUTER_NAMESPACE::flowgraph::MonoBlend> mMonoBlend;
- std::unique_ptr<FLOWGRAPH_OUTER_NAMESPACE::flowgraph::ClipToRange> mClipper;
+ std::unique_ptr<FLOWGRAPH_OUTER_NAMESPACE::flowgraph::Limiter> mLimiter;
std::unique_ptr<FLOWGRAPH_OUTER_NAMESPACE::flowgraph::MonoToMultiConverter> mChannelConverter;
std::unique_ptr<FLOWGRAPH_OUTER_NAMESPACE::flowgraph::ManyToMultiConverter>
mManyToMultiConverter;
diff --git a/media/libaaudio/src/flowgraph/Limiter.cpp b/media/libaaudio/src/flowgraph/Limiter.cpp
new file mode 100644
index 0000000..def905a
--- /dev/null
+++ b/media/libaaudio/src/flowgraph/Limiter.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <algorithm>
+#include <math.h>
+#include <unistd.h>
+#include "FlowGraphNode.h"
+#include "Limiter.h"
+
+using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph;
+
+Limiter::Limiter(int32_t channelCount)
+ : FlowGraphFilter(channelCount) {
+}
+
+int32_t Limiter::onProcess(int32_t numFrames) {
+ const float *inputBuffer = input.getBuffer();
+ float *outputBuffer = output.getBuffer();
+
+ int32_t numSamples = numFrames * output.getSamplesPerFrame();
+
+ // Cache the last valid output to reduce memory read/write
+ float lastValidOutput = mLastValidOutput;
+
+ for (int32_t i = 0; i < numSamples; i++) {
+ // Use the previous output if the input is NaN
+ if (!isnan(*inputBuffer)) {
+ lastValidOutput = processFloat(*inputBuffer);
+ }
+ inputBuffer++;
+ *outputBuffer++ = lastValidOutput;
+ }
+ mLastValidOutput = lastValidOutput;
+
+ return numFrames;
+}
+
+float Limiter::processFloat(float in)
+{
+ float in_abs = fabsf(in);
+ if (in_abs <= 1) {
+ return in;
+ }
+ float out;
+ if (in_abs < kXWhenYis3Decibels) {
+ out = (kPolynomialSplineA * in_abs + kPolynomialSplineB) * in_abs + kPolynomialSplineC;
+ } else {
+ out = M_SQRT2;
+ }
+ if (in < 0) {
+ out = -out;
+ }
+ return out;
+}
diff --git a/media/libaaudio/src/flowgraph/Limiter.h b/media/libaaudio/src/flowgraph/Limiter.h
new file mode 100644
index 0000000..393a7bf
--- /dev/null
+++ b/media/libaaudio/src/flowgraph/Limiter.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLOWGRAPH_LIMITER_H
+#define FLOWGRAPH_LIMITER_H
+
+#include <atomic>
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "FlowGraphNode.h"
+
+namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph {
+
+class Limiter : public FlowGraphFilter {
+public:
+ explicit Limiter(int32_t channelCount);
+
+ int32_t onProcess(int32_t numFrames) override;
+
+ const char *getName() override {
+ return "Limiter";
+ }
+
+private:
+ // These numbers are based on a polynomial spline for a quadratic solution Ax^2 + Bx + C
+ // The range is up to 3 dB, (10^(3/20)), to match AudioTrack for float data.
+ static constexpr float kPolynomialSplineA = -0.6035533905; // -(1+sqrt(2))/4
+ static constexpr float kPolynomialSplineB = 2.2071067811; // (3+sqrt(2))/2
+ static constexpr float kPolynomialSplineC = -0.6035533905; // -(1+sqrt(2))/4
+ static constexpr float kXWhenYis3Decibels = 1.8284271247; // -1+2sqrt(2)
+
+ /**
+ * Process an input based on the following:
+ * If between -1 and 1, return the input value.
+ * If above kXWhenYis3Decibels, return sqrt(2).
+ * If below -kXWhenYis3Decibels, return -sqrt(2).
+ * If between 1 and kXWhenYis3Decibels, use a quadratic spline (Ax^2 + Bx + C).
+ * If between -kXWhenYis3Decibels and -1, use the absolute value for the spline and flip it.
+ * The derivative of the spline is 1 at 1 and 0 at kXWhenYis3Decibels.
+ * This way, the graph is both continuous and differentiable.
+ */
+ float processFloat(float in);
+
+ // Use the previous valid output for NaN inputs
+ float mLastValidOutput = 0.0f;
+};
+
+} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */
+
+#endif //FLOWGRAPH_LIMITER_H
diff --git a/media/libaaudio/tests/test_flowgraph.cpp b/media/libaaudio/tests/test_flowgraph.cpp
index 66b77eb..6f75f5a 100644
--- a/media/libaaudio/tests/test_flowgraph.cpp
+++ b/media/libaaudio/tests/test_flowgraph.cpp
@@ -26,6 +26,7 @@
#include <gtest/gtest.h>
#include "flowgraph/ClipToRange.h"
+#include "flowgraph/Limiter.h"
#include "flowgraph/MonoBlend.h"
#include "flowgraph/MonoToMultiConverter.h"
#include "flowgraph/SourceFloat.h"
@@ -319,3 +320,77 @@
}
}
+TEST(test_flowgraph, module_limiter) {
+ constexpr int kNumSamples = 101;
+ constexpr float kLastSample = 3.0f;
+ constexpr float kFirstSample = -kLastSample;
+ constexpr float kDeltaBetweenSamples = (kLastSample - kFirstSample) / (kNumSamples - 1);
+ constexpr float kTolerance = 0.00001f;
+
+ float input[kNumSamples];
+ float output[kNumSamples];
+ SourceFloat sourceFloat{1};
+ Limiter limiter{1};
+ SinkFloat sinkFloat{1};
+
+ for (int i = 0; i < kNumSamples; i++) {
+ input[i] = kFirstSample + i * kDeltaBetweenSamples;
+ }
+
+ const int numInputFrames = std::size(input);
+ sourceFloat.setData(input, numInputFrames);
+
+ sourceFloat.output.connect(&limiter.input);
+ limiter.output.connect(&sinkFloat.input);
+
+ const int numOutputFrames = std::size(output);
+ int32_t numRead = sinkFloat.read(output, numOutputFrames);
+ ASSERT_EQ(numInputFrames, numRead);
+
+ for (int i = 0; i < numRead; i++) {
+ // limiter must be symmetric wrt 0.
+ EXPECT_NEAR(output[i], -output[kNumSamples - i - 1], kTolerance);
+ if (i > 0) {
+ EXPECT_GE(output[i], output[i - 1]); // limiter must be monotonic
+ }
+ if (input[i] == 0.f) {
+ EXPECT_EQ(0.f, output[i]);
+ } else if (input[i] > 0.0f) {
+ EXPECT_GE(output[i], 0.0f);
+ EXPECT_LE(output[i], M_SQRT2); // limiter actually limits
+ EXPECT_LE(output[i], input[i]); // a limiter, gain <= 1
+ } else {
+ EXPECT_LE(output[i], 0.0f);
+ EXPECT_GE(output[i], -M_SQRT2); // limiter actually limits
+ EXPECT_GE(output[i], input[i]); // a limiter, gain <= 1
+ }
+ if (-1.f <= input[i] && input[i] <= 1.f) {
+ EXPECT_EQ(input[i], output[i]);
+ }
+ }
+}
+
+TEST(test_flowgraph, module_limiter_nan) {
+ constexpr int kArbitraryOutputSize = 100;
+ static const float input[] = {NAN, 0.5f, NAN, NAN, -10.0f, NAN};
+ static const float expected[] = {0.0f, 0.5f, 0.5f, 0.5f, -M_SQRT2, -M_SQRT2};
+ constexpr float tolerance = 0.00001f;
+ float output[kArbitraryOutputSize];
+ SourceFloat sourceFloat{1};
+ Limiter limiter{1};
+ SinkFloat sinkFloat{1};
+
+ const int numInputFrames = std::size(input);
+ sourceFloat.setData(input, numInputFrames);
+
+ sourceFloat.output.connect(&limiter.input);
+ limiter.output.connect(&sinkFloat.input);
+
+ const int numOutputFrames = std::size(output);
+ int32_t numRead = sinkFloat.read(output, numOutputFrames);
+ ASSERT_EQ(numInputFrames, numRead);
+
+ for (int i = 0; i < numRead; i++) {
+ EXPECT_NEAR(expected[i], output[i], tolerance);
+ }
+}