From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- media/libcubeb/test/test_loopback.cpp | 596 ++++++++++++++++++++++++++++++++++ 1 file changed, 596 insertions(+) create mode 100644 media/libcubeb/test/test_loopback.cpp (limited to 'media/libcubeb/test/test_loopback.cpp') diff --git a/media/libcubeb/test/test_loopback.cpp b/media/libcubeb/test/test_loopback.cpp new file mode 100644 index 0000000000..bbb9409e67 --- /dev/null +++ b/media/libcubeb/test/test_loopback.cpp @@ -0,0 +1,596 @@ +/* + * Copyright © 2017 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + + /* libcubeb api/function test. Requests a loopback device and checks that + output is being looped back to input. NOTE: Usage of output devices while + performing this test will cause flakey results! */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include +#include +#include +#include +#include +#include +#include +#include "cubeb/cubeb.h" +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" +const uint32_t SAMPLE_FREQUENCY = 48000; +const uint32_t TONE_FREQUENCY = 440; +const double OUTPUT_AMPLITUDE = 0.25; +const int32_t NUM_FRAMES_TO_OUTPUT = SAMPLE_FREQUENCY / 20; /* play ~50ms of samples */ + +template T ConvertSampleToOutput(double input); +template<> float ConvertSampleToOutput(double input) { return float(input); } +template<> short ConvertSampleToOutput(double input) { return short(input * 32767.0f); } + +template double ConvertSampleFromOutput(T sample); +template<> double ConvertSampleFromOutput(float sample) { return double(sample); } +template<> double ConvertSampleFromOutput(short sample) { return double(sample / 32767.0); } + +/* Simple cross correlation to help find phase shift. Not a performant impl */ +std::vector cross_correlate(std::vector & f, + std::vector & g, + size_t signal_length) +{ + /* the length we sweep our window through to find the cross correlation */ + size_t sweep_length = f.size() - signal_length + 1; + std::vector correlation; + correlation.reserve(sweep_length); + for (size_t i = 0; i < sweep_length; i++) { + double accumulator = 0.0; + for (size_t j = 0; j < signal_length; j++) { + accumulator += f.at(j) * g.at(i + j); + } + correlation.push_back(accumulator); + } + return correlation; +} + +/* best effort discovery of phase shift between output and (looped) input*/ +size_t find_phase(std::vector & output_frames, + std::vector & input_frames, + size_t signal_length) +{ + std::vector correlation = cross_correlate(output_frames, input_frames, signal_length); + size_t phase = 0; + double max_correlation = correlation.at(0); + for (size_t i = 1; i < correlation.size(); i++) { + if (correlation.at(i) > max_correlation) { + max_correlation = correlation.at(i); + phase = i; + } + } + return phase; +} + +std::vector normalize_frames(std::vector & frames) { + double max = abs(*std::max_element(frames.begin(), frames.end(), + [](double a, double b) { return abs(a) < abs(b); })); + std::vector normalized_frames; + normalized_frames.reserve(frames.size()); + for (const double frame : frames) { + normalized_frames.push_back(frame / max); + } + return normalized_frames; +} + +/* heuristic comparison of aligned output and input signals, gets flaky if TONE_FREQUENCY is too high */ +void compare_signals(std::vector & output_frames, + std::vector & input_frames) +{ + ASSERT_EQ(output_frames.size(), input_frames.size()) << "#Output frames != #input frames"; + size_t num_frames = output_frames.size(); + std::vector normalized_output_frames = normalize_frames(output_frames); + std::vector normalized_input_frames = normalize_frames(input_frames); + + /* calculate mean absolute errors */ + /* mean absolute errors between output and input */ + double io_mas = 0.0; + /* mean absolute errors between output and silence */ + double output_silence_mas = 0.0; + /* mean absolute errors between input and silence */ + double input_silence_mas = 0.0; + for (size_t i = 0; i < num_frames; i++) { + io_mas += abs(normalized_output_frames.at(i) - normalized_input_frames.at(i)); + output_silence_mas += abs(normalized_output_frames.at(i)); + input_silence_mas += abs(normalized_input_frames.at(i)); + } + io_mas /= num_frames; + output_silence_mas /= num_frames; + input_silence_mas /= num_frames; + + ASSERT_LT(io_mas, output_silence_mas) + << "Error between output and input should be less than output and silence!"; + ASSERT_LT(io_mas, input_silence_mas) + << "Error between output and input should be less than output and silence!"; + + /* make sure extrema are in (roughly) correct location */ + /* number of maxima + minama expected in the frames*/ + const long NUM_EXTREMA = 2 * TONE_FREQUENCY * NUM_FRAMES_TO_OUTPUT / SAMPLE_FREQUENCY; + /* expected index of first maxima */ + const long FIRST_MAXIMUM_INDEX = SAMPLE_FREQUENCY / TONE_FREQUENCY / 4; + /* Threshold we expect all maxima and minima to be above or below. Ideally + the extrema would be 1 or -1, but particularly at the start of loopback + the values seen can be significantly lower. */ + const double THRESHOLD = 0.5; + + for (size_t i = 0; i < NUM_EXTREMA; i++) { + bool is_maximum = i % 2 == 0; + /* expected offset to current extreme: i * stide between extrema */ + size_t offset = i * SAMPLE_FREQUENCY / TONE_FREQUENCY / 2; + if (is_maximum) { + ASSERT_GT(normalized_output_frames.at(FIRST_MAXIMUM_INDEX + offset), THRESHOLD) + << "Output frames have unexpected missing maximum!"; + ASSERT_GT(normalized_input_frames.at(FIRST_MAXIMUM_INDEX + offset), THRESHOLD) + << "Input frames have unexpected missing maximum!"; + } else { + ASSERT_LT(normalized_output_frames.at(FIRST_MAXIMUM_INDEX + offset), -THRESHOLD) + << "Output frames have unexpected missing minimum!"; + ASSERT_LT(normalized_input_frames.at(FIRST_MAXIMUM_INDEX + offset), -THRESHOLD) + << "Input frames have unexpected missing minimum!"; + } + } +} + +struct user_state_loopback { + std::mutex user_state_mutex; + long position = 0; + /* track output */ + std::vector output_frames; + /* track input */ + std::vector input_frames; +}; + +template +long data_cb_loop_duplex(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *) user; + T * ib = (T *) inputbuffer; + T * ob = (T *) outputbuffer; + + if (stream == NULL || inputbuffer == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard lock(u->user_state_mutex); + /* generate our test tone on the fly */ + for (int i = 0; i < nframes; i++) { + double tone = 0.0; + if (u->position + i < NUM_FRAMES_TO_OUTPUT) { + /* generate sine wave */ + tone = sin(2 * M_PI*(i + u->position) * TONE_FREQUENCY / SAMPLE_FREQUENCY); + tone *= OUTPUT_AMPLITUDE; + } + ob[i] = ConvertSampleToOutput(tone); + u->output_frames.push_back(tone); + /* store any looped back output, may be silence */ + u->input_frames.push_back(ConvertSampleFromOutput(ib[i])); + } + + u->position += nframes; + + return nframes; +} + +template +long data_cb_loop_input_only(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *) user; + T * ib = (T *) inputbuffer; + + if (outputbuffer != NULL) { + // Can't assert as it needs to return, so expect to fail instead + EXPECT_EQ(outputbuffer, (void *) NULL) << "outputbuffer should be null in input only callback"; + return CUBEB_ERROR; + } + + if (stream == NULL || inputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard lock(u->user_state_mutex); + for (int i = 0; i < nframes; i++) { + u->input_frames.push_back(ConvertSampleFromOutput(ib[i])); + } + + return nframes; +} + +template +long data_cb_playback(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *) user; + T * ob = (T *) outputbuffer; + + if (stream == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard lock(u->user_state_mutex); + /* generate our test tone on the fly */ + for (int i = 0; i < nframes; i++) { + double tone = 0.0; + if (u->position + i < NUM_FRAMES_TO_OUTPUT) { + /* generate sine wave */ + tone = sin(2 * M_PI*(i + u->position) * TONE_FREQUENCY / SAMPLE_FREQUENCY); + tone *= OUTPUT_AMPLITUDE; + } + ob[i] = ConvertSampleToOutput(tone); + u->output_frames.push_back(tone); + } + + u->position += nframes; + + return nframes; +} + +void state_cb_loop(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + if (stream == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +void run_loopback_duplex_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: duplex stream"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup a duplex stream with loopback */ + r = cubeb_stream_init(ctx, &stream, "Cubeb loopback", + NULL, &input_params, NULL, &output_params, latency_frames, + is_float ? data_cb_loop_duplex : data_cb_loop_duplex, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(300); + cubeb_stream_stop(stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & output_frames = user_data->output_frames; + std::vector & input_frames = user_data->input_frames; + ASSERT_EQ(output_frames.size(), input_frames.size()) + << "#Output frames != #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_output_frames(output_frames_signal_start, output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_input_frames(input_frames_signal_start, input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_duplex) +{ + run_loopback_duplex_test(true); + run_loopback_duplex_test(false); +} + +void run_loopback_separate_streams_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * input_stream; + cubeb_stream * output_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: separate streams"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", + NULL, &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only : data_cb_loop_input_only, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + /* setup an output stream */ + r = cubeb_stream_init(ctx, &output_stream, "Cubeb loopback output only", + NULL, NULL, NULL, &output_params, latency_frames, + is_float ? data_cb_playback : data_cb_playback, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_output_stream_at_exit(output_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + cubeb_stream_start(output_stream); + delay(300); + cubeb_stream_stop(output_stream); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & output_frames = user_data->output_frames; + std::vector & input_frames = user_data->input_frames; + ASSERT_LE(output_frames.size(), input_frames.size()) + << "#Output frames should be less or equal to #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_output_frames(output_frames_signal_start, output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_input_frames(input_frames_signal_start, input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_separate_streams) +{ + run_loopback_separate_streams_test(true); + run_loopback_separate_streams_test(false); +} + +void run_loopback_silence_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * input_stream; + cubeb_stream_params input_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: record silence"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &input_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", + NULL, &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only : data_cb_loop_input_only, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + delay(300); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & input_frames = user_data->input_frames; + + /* expect to have at least ~50ms of frames */ + ASSERT_GE(input_frames.size(), SAMPLE_FREQUENCY / 20); + double EPISILON = 0.0001; + /* frames should be 0.0, but use epsilon to avoid possible issues with impls + that may use ~0.0 silence values. */ + for (double frame : input_frames) { + ASSERT_LT(abs(frame), EPISILON); + } +} + +TEST(cubeb, loopback_silence) +{ + run_loopback_silence_test(true); + run_loopback_silence_test(false); +} + +void run_loopback_device_selection_test(bool is_float) +{ + cubeb * ctx; + cubeb_device_collection collection; + cubeb_stream * input_stream; + cubeb_stream * output_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: device selection, separate streams"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + fprintf(stderr, "Device enumeration not supported" + " for this backend, skipping this test.\n"); + return; + } + + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + /* get first preferred output device id */ + std::string device_id; + for (size_t i = 0; i < collection.count; i++) { + if (collection.device[i].preferred) { + device_id = collection.device[i].device_id; + break; + } + } + cubeb_device_collection_destroy(ctx, &collection); + if (device_id.empty()) { + fprintf(stderr, "Could not find preferred device, aborting test.\n"); + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", + device_id.c_str(), &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only : data_cb_loop_input_only, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + /* setup an output stream */ + r = cubeb_stream_init(ctx, &output_stream, "Cubeb loopback output only", + NULL, NULL, device_id.c_str(), &output_params, latency_frames, + is_float ? data_cb_playback : data_cb_playback, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_output_stream_at_exit(output_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + cubeb_stream_start(output_stream); + delay(300); + cubeb_stream_stop(output_stream); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & output_frames = user_data->output_frames; + std::vector & input_frames = user_data->input_frames; + ASSERT_LE(output_frames.size(), input_frames.size()) + << "#Output frames should be less or equal to #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_output_frames(output_frames_signal_start, output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_input_frames(input_frames_signal_start, input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_device_selection) +{ + run_loopback_device_selection_test(true); + run_loopback_device_selection_test(false); +} -- cgit v1.2.3