aboutsummaryrefslogtreecommitdiff
path: root/tests/src/pcm_loopback_test.cc
diff options
context:
space:
mode:
Diffstat (limited to 'tests/src/pcm_loopback_test.cc')
-rw-r--r--tests/src/pcm_loopback_test.cc230
1 files changed, 230 insertions, 0 deletions
diff --git a/tests/src/pcm_loopback_test.cc b/tests/src/pcm_loopback_test.cc
new file mode 100644
index 0000000..6a3ffb8
--- /dev/null
+++ b/tests/src/pcm_loopback_test.cc
@@ -0,0 +1,230 @@
+/* pcm_loopback_test.c
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in the
+** documentation and/or other materials provided with the distribution.
+** * Neither the name of The Android Open Source Project nor the names of
+** its contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+#include "pcm_test_device.h"
+
+#include <chrono>
+#include <cmath>
+#include <cstring>
+#include <iostream>
+#include <thread>
+
+#include <gtest/gtest.h>
+
+#include "tinyalsa/pcm.h"
+
+namespace tinyalsa {
+namespace testing {
+
+template<int32_t CH, int32_t SR, pcm_format F>
+class SilenceGenerator {
+public:
+ pcm_format GetFormat() {
+ return F;
+ }
+
+ int32_t GetChannels() {
+ return CH;
+ };
+
+ int32_t GetSamplingRate() {
+ return SR;
+ };
+
+ virtual int32_t Read(void *buffer, int32_t size) {
+ std::memset(buffer, 0, size);
+ return size;
+ }
+};
+
+template<pcm_format F>
+struct PcmFormat {
+ using Type = void;
+ static constexpr int32_t kMax = 0;
+ static constexpr int32_t kMin = 0;
+};
+
+template<>
+struct PcmFormat<PCM_FORMAT_S16_LE> {
+ using Type = int16_t;
+ static constexpr Type kMax = std::numeric_limits<Type>::max();
+ static constexpr Type kMin = std::numeric_limits<Type>::min();
+};
+
+// CH: channels
+// SR: sampling rate
+// FQ: sine wave frequency
+// L: max level
+template<int32_t CH, int32_t SR, int32_t FQ, int32_t L, pcm_format F>
+class SineToneGenerator : public SilenceGenerator<CH, SR, F> {
+private:
+ using Type = typename PcmFormat<F>::Type;
+ static constexpr double kPi = M_PI;
+ static constexpr double kStep = FQ * CH * kPi / SR;
+
+ double channels[CH];
+ double gain;
+
+ Type GetSample(double radian) {
+ double sine = std::sin(radian) * gain;
+ if (sine >= 1.0) {
+ return PcmFormat<F>::kMax;
+ } else if (sine <= -1.0) {
+ return PcmFormat<F>::kMin;
+ }
+ return static_cast<Type>(sine * PcmFormat<F>::kMax);
+ }
+
+public:
+ SineToneGenerator() {
+ constexpr double phase = (CH == 1) ? 0 : kPi / 2 / (CH - 1);
+
+ channels[0] = 0.0;
+ for (int32_t i = 1; i < CH; ++i) {
+ channels[i] = channels[i - 1] + phase;
+ }
+
+ gain = std::pow(M_E, std::log(10) * static_cast<double>(L) / 20.0);
+ }
+
+ ~SineToneGenerator() = default;
+
+ int32_t Read(void *buffer, int32_t size) override {
+ Type *pcm_buffer = reinterpret_cast<Type *>(buffer);
+
+ size = (size / (CH * sizeof(Type))) * (CH * sizeof(Type));
+ int32_t samples = size / sizeof(Type);
+ int32_t s = 0;
+
+ while (s < samples) {
+ for (int32_t i = 0; i < CH; ++i) {
+ pcm_buffer[s++] = GetSample(channels[i]);
+ channels[i] += kStep;
+ }
+ }
+ return size;
+ }
+};
+
+template<typename T>
+static double Energy(T *buffer, size_t samples) {
+ double sum = 0.0;
+ for (size_t i = 0; i < samples; i++) {
+ sum += static_cast<double>(buffer[i]) * static_cast<double>(buffer[i]);
+ }
+ return sum;
+}
+
+TEST(PcmLoopbackTest, LoopbackS16le) {
+ static constexpr unsigned int kDefaultChannels = 2;
+ static constexpr unsigned int kDefaultSamplingRate = 48000;
+ static constexpr unsigned int kDefaultPeriodSize = 1024;
+ static constexpr unsigned int kDefaultPeriodCount = 3;
+ static constexpr unsigned int kDefaultPeriodTimeInMs =
+ kDefaultPeriodSize * 1000 / kDefaultSamplingRate;
+
+ static constexpr pcm_config kInConfig = {
+ .channels = kDefaultChannels,
+ .rate = kDefaultSamplingRate,
+ .period_size = kDefaultPeriodSize,
+ .period_count = kDefaultPeriodCount,
+ .format = PCM_FORMAT_S16_LE,
+ .start_threshold = 0,
+ .stop_threshold = 0,
+ .silence_threshold = 0,
+ .silence_size = 0,
+ };
+ pcm *pcm_in = pcm_open(kLoopbackCard, kLoopbackCaptureDevice, PCM_IN, &kInConfig);
+ ASSERT_TRUE(pcm_is_ready(pcm_in));
+
+ static constexpr pcm_config kOutConfig = {
+ .channels = kDefaultChannels,
+ .rate = kDefaultSamplingRate,
+ .period_size = kDefaultPeriodSize,
+ .period_count = kDefaultPeriodCount,
+ .format = PCM_FORMAT_S16_LE,
+ .start_threshold = kDefaultPeriodSize,
+ .stop_threshold = kDefaultPeriodSize * kDefaultPeriodCount,
+ .silence_threshold = 0,
+ .silence_size = 0,
+ };
+ pcm *pcm_out = pcm_open(kLoopbackCard, kLoopbackPlaybackDevice, PCM_OUT, &kOutConfig);
+ ASSERT_TRUE(pcm_is_ready(pcm_out));
+
+ ASSERT_EQ(pcm_link(pcm_in, pcm_out), 0);
+
+ bool stopping = false;
+ ASSERT_EQ(pcm_get_subdevice(pcm_in), pcm_get_subdevice(pcm_out));
+
+ std::thread capture([pcm_in, &stopping] {
+ size_t buffer_size = pcm_frames_to_bytes(pcm_in, kDefaultPeriodSize);
+ unsigned int frames = pcm_bytes_to_frames(pcm_in, buffer_size);
+ auto buffer = std::make_unique<unsigned char[]>(buffer_size);
+ int32_t counter = 0;
+ while (!stopping) {
+ int res = pcm_readi(pcm_in, buffer.get(), frames);
+ if (res == -1) {
+ std::cout << pcm_get_error(pcm_in) << std::endl;
+ std::this_thread::sleep_for(std::chrono::milliseconds(kDefaultPeriodTimeInMs));
+ continue;
+ }
+ EXPECT_EQ(pcm_readi(pcm_in, buffer.get(), frames), frames) << counter;
+ // Test the energy of the buffer after the sine tone samples fill in the buffer.
+ // Therefore, check the buffer 5 times later.
+ if (counter >= 5) {
+ double e = Energy(buffer.get(), frames * kInConfig.channels);
+ EXPECT_GT(e, 0.0) << counter;
+ }
+ counter++;
+ }
+ });
+
+ std::thread playback([pcm_out, &stopping] {
+ SineToneGenerator<2, 48000, 1000, 0, PCM_FORMAT_S16_LE> generator;
+ size_t buffer_size = pcm_frames_to_bytes(pcm_out, kDefaultPeriodSize);
+ unsigned int frames = pcm_bytes_to_frames(pcm_out, buffer_size);
+ auto buffer = std::make_unique<unsigned char[]>(buffer_size);
+ int32_t counter = 0;
+ while (!stopping) {
+ generator.Read(buffer.get(), buffer_size);
+ EXPECT_EQ(pcm_writei(pcm_out, buffer.get(), frames), frames) << counter;
+ counter++;
+ }
+ });
+
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+ stopping = true;
+ capture.join();
+ playback.join();
+
+ ASSERT_EQ(pcm_unlink(pcm_in), 0);
+ pcm_close(pcm_in);
+ pcm_close(pcm_out);
+}
+
+} // namespace testing
+} // namespace tinyalsa