diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /media/libcubeb/test | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
20 files changed, 5089 insertions, 0 deletions
diff --git a/media/libcubeb/test/README.md b/media/libcubeb/test/README.md new file mode 100644 index 0000000000..7318263989 --- /dev/null +++ b/media/libcubeb/test/README.md @@ -0,0 +1,13 @@ +Notes on writing tests. + +The googletest submodule is currently at 1.6 rather than the latest, and should +only be updated to track the version used in Gecko to make test compatibility +easier. + +Always #include "gtest/gtest.h" before *anything* else. + +All tests should be part of the "cubeb" test case, e.g. TEST(cubeb, my_test). + +Tests are built stand-alone in cubeb, but built as a single unit in Gecko, so +you must use unique names for globally visible items in each test, e.g. rather +than state_cb use state_cb_my_test. diff --git a/media/libcubeb/test/common.h b/media/libcubeb/test/common.h new file mode 100644 index 0000000000..76d318cd98 --- /dev/null +++ b/media/libcubeb/test/common.h @@ -0,0 +1,181 @@ +/* + * Copyright © 2013 Sebastien Alaiwan + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#if !defined(TEST_COMMON) +#define TEST_COMMON + +#if defined( _WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <objbase.h> +#include <windows.h> +#else +#include <unistd.h> +#endif + +#include <cstdarg> +#include "cubeb/cubeb.h" +#include "cubeb_mixer.h" + +template<typename T, size_t N> +constexpr size_t +ARRAY_LENGTH(T(&)[N]) +{ + return N; +} + +inline void delay(unsigned int ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + sleep(ms / 1000); + usleep(ms % 1000 * 1000); +#endif +} + +#if !defined(M_PI) +#define M_PI 3.14159265358979323846 +#endif + +typedef struct { + char const * name; + unsigned int const channels; + uint32_t const layout; +} layout_info; + +struct backend_caps { + const char* id; + const int input_capabilities; +}; + + +// This static table allows knowing if a backend has audio input capabilities. +// We don't rely on opening a stream and checking if it works, because this +// would make the test skip the tests that make use of audio input, if a +// particular backend has a bug that causes a failure during audio input stream +// creation +static backend_caps backend_capabilities[] = { + {"sun", 1}, + {"wasapi", 1}, + {"kai", 1}, + {"audiounit", 1}, + {"audiotrack", 0}, + {"opensl", 1}, + {"aaudio", 1}, + {"jack", 1}, + {"pulse", 1}, + {"sndio", 1}, + {"oss", 1}, + {"winmm", 0}, + {"alsa", 1}, +}; + +inline int can_run_audio_input_test(cubeb * ctx) +{ + cubeb_device_collection devices; + int input_device_available = 0; + int r; + /* Bail out early if the host does not have input devices. */ + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_INPUT, &devices); + if (r != CUBEB_OK) { + fprintf(stderr, "error enumerating devices."); + return 0; + } + + if (devices.count == 0) { + fprintf(stderr, "no input device available, skipping test.\n"); + cubeb_device_collection_destroy(ctx, &devices); + return 0; + } + + for (uint32_t i = 0; i < devices.count; i++) { + input_device_available |= (devices.device[i].state == + CUBEB_DEVICE_STATE_ENABLED); + } + + if (!input_device_available) { + fprintf(stderr, "there are input devices, but they are not " + "available, skipping\n"); + } + + cubeb_device_collection_destroy(ctx, &devices); + + int backend_has_input_capabilities; + const char * backend_id = cubeb_get_backend_id(ctx); + for (uint32_t i = 0; i < sizeof(backend_capabilities) / sizeof(backend_caps); i++) { + if (strcmp(backend_capabilities[i].id, backend_id) == 0) { + backend_has_input_capabilities = backend_capabilities[i].input_capabilities; + } + } + + return !!input_device_available && !!backend_has_input_capabilities; +} + +inline void print_log(const char * msg, ...) +{ + va_list args; + va_start(args, msg); + vprintf(msg, args); + va_end(args); +} + +/** Initialize cubeb with backend override. + * Create call cubeb_init passing value for CUBEB_BACKEND env var as + * override. */ +inline int common_init(cubeb ** ctx, char const * ctx_name) +{ +#ifdef ENABLE_NORMAL_LOG + if (cubeb_set_log_callback(CUBEB_LOG_NORMAL, print_log) != CUBEB_OK) { + fprintf(stderr, "Set normal log callback failed\n"); + } +#endif + +#ifdef ENABLE_VERBOSE_LOG + if (cubeb_set_log_callback(CUBEB_LOG_VERBOSE, print_log) != CUBEB_OK) { + fprintf(stderr, "Set verbose log callback failed\n"); + } +#endif + + int r; + char const * backend; + char const * ctx_backend; + + backend = getenv("CUBEB_BACKEND"); + r = cubeb_init(ctx, ctx_name, backend); + if (r == CUBEB_OK && backend) { + ctx_backend = cubeb_get_backend_id(*ctx); + if (strcmp(backend, ctx_backend) != 0) { + fprintf(stderr, "Requested backend `%s', got `%s'\n", + backend, ctx_backend); + } + } + + return r; +} + +#if defined( _WIN32) +class TestEnvironment : public ::testing::Environment { +public: + void SetUp() override { + hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + } + + void TearDown() override { + if (SUCCEEDED(hr)) { + CoUninitialize(); + } + } + +private: + HRESULT hr; +}; + +::testing::Environment* const foo_env = ::testing::AddGlobalTestEnvironment(new TestEnvironment); +#endif + +#endif /* TEST_COMMON */ diff --git a/media/libcubeb/test/test_audio.cpp b/media/libcubeb/test/test_audio.cpp new file mode 100644 index 0000000000..bc4b435712 --- /dev/null +++ b/media/libcubeb/test/test_audio.cpp @@ -0,0 +1,243 @@ +/* + * Copyright © 2013 Sebastien Alaiwan <sebastien.alaiwan@gmail.com> + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function exhaustive test. Plays a series of tones in different + * conditions. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <memory> +#include <string.h> +#include "cubeb/cubeb.h" +#include <string> + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +using namespace std; + +#define MAX_NUM_CHANNELS 32 +#define VOLUME 0.2 + +float get_frequency(int channel_index) +{ + return 220.0f * (channel_index+1); +} + +template<typename T> T ConvertSample(double input); +template<> float ConvertSample(double input) { return input; } +template<> short ConvertSample(double input) { return short(input * 32767.0f); } + +/* store the phase of the generated waveform */ +struct synth_state { + synth_state(int num_channels_, float sample_rate_) + : num_channels(num_channels_), + sample_rate(sample_rate_) + { + for(int i=0;i < MAX_NUM_CHANNELS;++i) + phase[i] = 0.0f; + } + + template<typename T> + void run(T* audiobuffer, long nframes) + { + for(int c=0;c < num_channels;++c) { + float freq = get_frequency(c); + float phase_inc = 2.0 * M_PI * freq / sample_rate; + for(long n=0;n < nframes;++n) { + audiobuffer[n*num_channels+c] = ConvertSample<T>(sin(phase[c]) * VOLUME); + phase[c] += phase_inc; + } + } + } + +private: + int num_channels; + float phase[MAX_NUM_CHANNELS]; + float sample_rate; +}; + +template<typename T> +long data_cb(cubeb_stream * /*stream*/, void * user, const void * /*inputbuffer*/, void * outputbuffer, long nframes) +{ + synth_state *synth = (synth_state *)user; + synth->run((T*)outputbuffer, nframes); + return nframes; +} + +void state_cb_audio(cubeb_stream * /*stream*/, void * /*user*/, cubeb_state /*state*/) +{ +} + +/* Our android backends don't support float, only int16. */ +int supports_float32(string backend_id) +{ + return backend_id != "opensl" + && backend_id != "audiotrack"; +} + +/* Some backends don't have code to deal with more than mono or stereo. */ +int supports_channel_count(string backend_id, int nchannels) +{ + return nchannels <= 2 || + (backend_id != "opensl" && backend_id != "audiotrack"); +} + +int run_test(int num_channels, int sampling_rate, int is_float) +{ + int r = CUBEB_OK; + + cubeb *ctx = NULL; + + r = common_init(&ctx, "Cubeb audio test: channels"); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb library\n"); + return r; + } + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + const char * backend_id = cubeb_get_backend_id(ctx); + + if ((is_float && !supports_float32(backend_id)) || + !supports_channel_count(backend_id, num_channels)) { + /* don't treat this as a test failure. */ + return CUBEB_OK; + } + + fprintf(stderr, "Testing %d channel(s), %d Hz, %s (%s)\n", num_channels, sampling_rate, is_float ? "float" : "short", cubeb_get_backend_id(ctx)); + + cubeb_stream_params params; + params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16NE; + params.rate = sampling_rate; + params.channels = num_channels; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + synth_state synth(params.channels, params.rate); + + cubeb_stream *stream = NULL; + r = cubeb_stream_init(ctx, &stream, "test tone", NULL, NULL, NULL, ¶ms, + 4096, is_float ? &data_cb<float> : &data_cb<short>, state_cb_audio, &synth); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb stream: %d\n", r); + return r; + } + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(200); + cubeb_stream_stop(stream); + + return r; +} + +int run_volume_test(int is_float) +{ + int r = CUBEB_OK; + + cubeb *ctx = NULL; + + r = common_init(&ctx, "Cubeb audio test"); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb library\n"); + return r; + } + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + const char * backend_id = cubeb_get_backend_id(ctx); + + if ((is_float && !supports_float32(backend_id))) { + /* don't treat this as a test failure. */ + return CUBEB_OK; + } + + cubeb_stream_params params; + params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16NE; + params.rate = 44100; + params.channels = 2; + params.layout = CUBEB_LAYOUT_STEREO; + params.prefs = CUBEB_STREAM_PREF_NONE; + + synth_state synth(params.channels, params.rate); + + cubeb_stream *stream = NULL; + r = cubeb_stream_init(ctx, &stream, "test tone", NULL, NULL, NULL, ¶ms, + 4096, is_float ? &data_cb<float> : &data_cb<short>, + state_cb_audio, &synth); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb stream: %d\n", r); + return r; + } + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + fprintf(stderr, "Testing: volume\n"); + for(int i=0;i <= 4; ++i) + { + fprintf(stderr, "Volume: %d%%\n", i*25); + + cubeb_stream_set_volume(stream, i/4.0f); + cubeb_stream_start(stream); + delay(400); + cubeb_stream_stop(stream); + delay(100); + } + + return r; +} + +TEST(cubeb, run_volume_test_short) +{ + ASSERT_EQ(run_volume_test(0), CUBEB_OK); +} + +TEST(cubeb, run_volume_test_float) +{ + ASSERT_EQ(run_volume_test(1), CUBEB_OK); +} + +TEST(cubeb, run_channel_rate_test) +{ + unsigned int channel_values[] = { + 1, + 2, + 3, + 4, + 6, + }; + + int freq_values[] = { + 16000, + 24000, + 44100, + 48000, + }; + + for(auto channels : channel_values) { + for(auto freq : freq_values) { + ASSERT_TRUE(channels < MAX_NUM_CHANNELS); + fprintf(stderr, "--------------------------\n"); + ASSERT_EQ(run_test(channels, freq, 0), CUBEB_OK); + ASSERT_EQ(run_test(channels, freq, 1), CUBEB_OK); + } + } +} + + +#undef MAX_NUM_CHANNELS +#undef VOLUME diff --git a/media/libcubeb/test/test_callback_ret.cpp b/media/libcubeb/test/test_callback_ret.cpp new file mode 100644 index 0000000000..49676f7fa2 --- /dev/null +++ b/media/libcubeb/test/test_callback_ret.cpp @@ -0,0 +1,239 @@ +/* +* 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. Test that different return values from user + specified callbacks are handled correctly. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include <memory> +#include <atomic> +#include <string> +#include "cubeb/cubeb.h" + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +const uint32_t SAMPLE_FREQUENCY = 48000; +const cubeb_sample_format SAMPLE_FORMAT = CUBEB_SAMPLE_S16NE; + +enum test_direction { + INPUT_ONLY, + OUTPUT_ONLY, + DUPLEX +}; + +// Structure which is used by data callbacks to track the total callbacks +// executed vs the number of callbacks expected. +struct user_state_callback_ret { + std::atomic<int> cb_count{ 0 }; + std::atomic<int> expected_cb_count{ 0 }; + std::atomic<int> error_state{ 0 }; +}; + +// Data callback that always returns 0 +long data_cb_ret_zero(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *) user; + // If this is the first time the callback has been called set our expected + // callback count + if (u->cb_count == 0) { + u->expected_cb_count = 1; + } + u->cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + return 0; +} + +// Data callback that always returns nframes - 1 +long data_cb_ret_nframes_minus_one(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *)user; + // If this is the first time the callback has been called set our expected + // callback count + if (u->cb_count == 0) { + u->expected_cb_count = 1; + } + u->cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + if (outputbuffer != NULL) { + // If we have an output buffer insert silence + short * ob = (short *) outputbuffer; + for (long i = 0; i < nframes - 1; i++) { + ob[i] = 0; + } + } + return nframes - 1; +} + +// Data callback that always returns nframes +long data_cb_ret_nframes(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *)user; + u->cb_count++; + // Every callback returns nframes, so every callback is expected + u->expected_cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + if (outputbuffer != NULL) { + // If we have an output buffer insert silence + short * ob = (short *) outputbuffer; + for (long i = 0; i < nframes; i++) { + ob[i] = 0; + } + } + return nframes; +} + +// Data callback that always returns CUBEB_ERROR +long +data_cb_ret_error(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *)user; + // If this is the first time the callback has been called set our expected + // callback count + if (u->cb_count == 0) { + u->expected_cb_count = 1; + } + u->cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + return CUBEB_ERROR; +} + +void state_cb_ret(cubeb_stream * stream, void * user, cubeb_state state) +{ + if (stream == NULL) + return; + user_state_callback_ret * u = (user_state_callback_ret *)user; + + 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; + case CUBEB_STATE_ERROR: + fprintf(stderr, "stream error\n"); + u->error_state.fetch_add(1); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } +} + +void run_test_callback(test_direction direction, + cubeb_data_callback data_cb, + const std::string & test_desc) { + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + user_state_callback_ret user_state; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb callback return value example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + if ((direction == INPUT_ONLY || direction == DUPLEX) && + !can_run_audio_input_test(ctx)) { + /* This test needs an available input device, skip it if this host does not + * have one or if the backend doesn't implement input. */ + return; + } + + // Setup all params, but only pass them later as required by direction + input_params.format = SAMPLE_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params = input_params; + + r = cubeb_get_min_latency(ctx, &input_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + switch (direction) + { + case INPUT_ONLY: + r = cubeb_stream_init(ctx, &stream, "Cubeb callback ret input", + NULL, &input_params, NULL, NULL, + latency_frames, data_cb, state_cb_ret, &user_state); + break; + case OUTPUT_ONLY: + r = cubeb_stream_init(ctx, &stream, "Cubeb callback ret output", + NULL, NULL, NULL, &output_params, + latency_frames, data_cb, state_cb_ret, &user_state); + break; + case DUPLEX: + r = cubeb_stream_init(ctx, &stream, "Cubeb callback ret duplex", + NULL, &input_params, NULL, &output_params, + latency_frames, data_cb, state_cb_ret, &user_state); + break; + default: + ASSERT_TRUE(false) << "Unrecognized test direction!"; + } + EXPECT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(100); + cubeb_stream_stop(stream); + + ASSERT_EQ(user_state.expected_cb_count, user_state.cb_count) << + "Callback called unexpected number of times for " << test_desc << "!"; + // TODO: On some test configurations, the data_callback is never called. + if (data_cb == data_cb_ret_error && user_state.cb_count != 0) { + ASSERT_EQ(user_state.error_state, 1) << "Callback expected error state"; + } +} + +TEST(cubeb, test_input_callback) +{ + run_test_callback(INPUT_ONLY, data_cb_ret_zero, "input only, return 0"); + run_test_callback(INPUT_ONLY, data_cb_ret_nframes_minus_one, "input only, return nframes - 1"); + run_test_callback(INPUT_ONLY, data_cb_ret_nframes, "input only, return nframes"); + run_test_callback(INPUT_ONLY, data_cb_ret_error, + "input only, return CUBEB_ERROR"); +} + +TEST(cubeb, test_output_callback) +{ + run_test_callback(OUTPUT_ONLY, data_cb_ret_zero, "output only, return 0"); + run_test_callback(OUTPUT_ONLY, data_cb_ret_nframes_minus_one, "output only, return nframes - 1"); + run_test_callback(OUTPUT_ONLY, data_cb_ret_nframes, "output only, return nframes"); + run_test_callback(OUTPUT_ONLY, data_cb_ret_error, + "output only, return CUBEB_ERROR"); +} + +TEST(cubeb, test_duplex_callback) +{ + run_test_callback(DUPLEX, data_cb_ret_zero, "duplex, return 0"); + run_test_callback(DUPLEX, data_cb_ret_nframes_minus_one, "duplex, return nframes - 1"); + run_test_callback(DUPLEX, data_cb_ret_nframes, "duplex, return nframes"); + run_test_callback(DUPLEX, data_cb_ret_error, "duplex, return CUBEB_ERROR"); +} diff --git a/media/libcubeb/test/test_deadlock.cpp b/media/libcubeb/test/test_deadlock.cpp new file mode 100644 index 0000000000..373ba6ad7e --- /dev/null +++ b/media/libcubeb/test/test_deadlock.cpp @@ -0,0 +1,262 @@ +/* + * Copyright © 2017 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + * + * + * Purpose + * ============================================================================= + * In CoreAudio, the data callback will holds a mutex shared with AudioUnit + * (mutex_AU). Thus, if the callback request another mutex M held by the another + * function, without releasing mutex_AU, then it will cause a deadlock when the + * another function, which holds the mutex M, request to use AudioUnit. + * + * The following figure illustrates the deadlock in bug 1337805: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1337805 + * (The detail analysis can be found on bug 1350511: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1350511) + * + * holds + * data_callback <---------- mutext_AudioUnit(mutex_AU) + * | ^ + * | | + * | request | request + * | | + * v holds | + * mutex_cubeb ------------> get_channel_layout + * + * In this example, the "audiounit_get_channel_layout" in f4edfb8: + * https://github.com/kinetiknz/cubeb/blob/f4edfb8eea920887713325e44773f3a2d959860c/src/cubeb_audiounit.cpp#L2725 + * requests the mutex_AU to create an AudioUnit, when it holds a mutex for cubeb + * context. Meanwhile, the data callback who holds the mutex_AU requests the + * mutex for cubeb context. As a result, it causes a deadlock. + * + * The problem is solve by pull 236: https://github.com/kinetiknz/cubeb/pull/236 + * We store the latest channel layout and return it when there is an active + * AudioUnit, otherwise, we will create an AudioUnit to get it. + * + * Although the problem is solved, to prevent it happens again, we add the test + * here in case someone without such knowledge misuses the AudioUnit in + * get_channel_layout. Moreover, it's a good way to record the known issues + * to warn other developers. + */ + +#include "gtest/gtest.h" +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" // for layout_infos +#include "cubeb/cubeb.h" // for cubeb utils +#include "cubeb_utils.h" // for owned_critical_section, auto_lock +#include <iostream> // for fprintf +#include <pthread.h> // for pthread +#include <signal.h> // for signal +#include <stdexcept> // for std::logic_error +#include <string> // for std::string +#include <unistd.h> // for sleep, usleep +#include <atomic> // for std::atomic + +// The signal alias for calling our thread killer. +#define CALL_THREAD_KILLER SIGUSR1 + +// This indicator will become true when our pending task thread is killed by +// ourselves. +bool killed = false; + +// This indicator will become true when the assigned task is done. +std::atomic<bool> task_done{ false }; + +// Indicating the data callback is fired or not. +bool called = false; + +// Toggle to true when running data callback. Before data callback gets +// the mutex for cubeb context, it toggles back to false. +// The task to get channel layout should be executed when this is true. +std::atomic<bool> callbacking_before_getting_context{ false }; + +owned_critical_section context_mutex; +cubeb * context = nullptr; + +cubeb * get_cubeb_context_unlocked() +{ + if (context) { + return context; + } + + int r = CUBEB_OK; + r = common_init(&context, "Cubeb deadlock test"); + if (r != CUBEB_OK) { + context = nullptr; + } + + return context; +} + +cubeb * get_cubeb_context() +{ + auto_lock lock(context_mutex); + return get_cubeb_context_unlocked(); +} + +void state_cb_audio(cubeb_stream * /*stream*/, void * /*user*/, cubeb_state /*state*/) +{ +} + +// Fired by coreaudio's rendering mechanism. It holds a mutex shared with the +// current used AudioUnit. +template<typename T> +long data_cb(cubeb_stream * /*stream*/, void * /*user*/, + const void * /*inputbuffer*/, void * outputbuffer, long nframes) +{ + called = true; + + uint64_t tid; // Current thread id. + pthread_threadid_np(NULL, &tid); + fprintf(stderr, "Audio output is on thread %llu\n", tid); + + if (!task_done) { + callbacking_before_getting_context = true; + fprintf(stderr, "[%llu] time to switch thread\n", tid); + // Force to switch threads by sleeping 10 ms. Notice that anything over + // 10ms would create a glitch. It's intended here for test, so the delay + // is ok. + usleep(10000); + callbacking_before_getting_context = false; + } + + fprintf(stderr, "[%llu] try getting backend id ...\n", tid); + + // Try requesting mutex for context by get_cubeb_context() + // when holding a mutex for AudioUnit. + char const * backend_id = cubeb_get_backend_id(get_cubeb_context()); + fprintf(stderr, "[%llu] callback on %s\n", tid, backend_id); + + // Mute the output (or get deaf) + memset(outputbuffer, 0, nframes * 2 * sizeof(float)); + return nframes; +} + +// Called by wait_to_get_layout, which is run out of main thread. +void get_preferred_channel_layout() +{ + auto_lock lock(context_mutex); + cubeb * context = get_cubeb_context_unlocked(); + ASSERT_TRUE(!!context); + + // We will cause a deadlock if cubeb_get_preferred_channel_layout requests + // mutex for AudioUnit when it holds mutex for context. + cubeb_channel_layout layout; + int r = cubeb_get_preferred_channel_layout(context, &layout); + ASSERT_EQ(r == CUBEB_OK, layout != CUBEB_LAYOUT_UNDEFINED); + fprintf(stderr, "layout is %s\n", layout_infos[layout].name); +} + +void * wait_to_get_layout(void *) +{ + uint64_t tid; // Current thread id. + pthread_threadid_np(NULL, &tid); + + while(!callbacking_before_getting_context) { + fprintf(stderr, "[%llu] waiting for data callback ...\n", tid); + usleep(1000); // Force to switch threads by sleeping 1 ms. + } + + fprintf(stderr, "[%llu] try getting channel layout ...\n", tid); + get_preferred_channel_layout(); // Deadlock checkpoint. + task_done = true; + + return NULL; +} + +void * watchdog(void * s) +{ + uint64_t tid; // Current thread id. + pthread_threadid_np(NULL, &tid); + + pthread_t subject = *((pthread_t *) s); + uint64_t stid; // task thread id. + pthread_threadid_np(subject, &stid); + + unsigned int sec = 2; + fprintf(stderr, "[%llu] sleep %d seconds before checking task for thread %llu\n", tid, sec, stid); + sleep(sec); // Force to switch threads. + + fprintf(stderr, "[%llu] check task for thread %llu now\n", tid, stid); + if (!task_done) { + fprintf(stderr, "[%llu] kill the task thread %llu\n", tid, stid); + pthread_kill(subject, CALL_THREAD_KILLER); + pthread_detach(subject); + // pthread_kill doesn't release the mutex held by the killed thread, + // so we need to unlock it manually. + context_mutex.unlock(); + } + fprintf(stderr, "[%llu] the assigned task for thread %llu is %sdone\n", tid, stid, (task_done) ? "" : "not "); + + return NULL; +} + +void thread_killer(int signal) +{ + ASSERT_EQ(signal, CALL_THREAD_KILLER); + fprintf(stderr, "task thread is killed!\n"); + killed = true; +} + +TEST(cubeb, run_deadlock_test) +{ +#if !defined(__APPLE__) + FAIL() << "Deadlock test is only for OSX now"; +#endif + + cubeb * ctx = get_cubeb_context(); + ASSERT_TRUE(!!ctx); + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + cubeb_stream_params params; + params.format = CUBEB_SAMPLE_FLOAT32NE; + params.rate = 44100; + params.channels = 2; + params.layout = CUBEB_LAYOUT_STEREO; + params.prefs = CUBEB_STREAM_PREF_NONE; + + cubeb_stream * stream = NULL; + int r = cubeb_stream_init(ctx, &stream, "test deadlock", NULL, NULL, NULL, + ¶ms, 512, &data_cb<float>, state_cb_audio, NULL); + ASSERT_EQ(r, CUBEB_OK); + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + // Install signal handler. + signal(CALL_THREAD_KILLER, thread_killer); + + pthread_t subject, detector; + pthread_create(&subject, NULL, wait_to_get_layout, NULL); + pthread_create(&detector, NULL, watchdog, (void *) &subject); + + uint64_t stid, dtid; + pthread_threadid_np(subject, &stid); + pthread_threadid_np(detector, &dtid); + fprintf(stderr, "task thread %llu, monitor thread %llu are created\n", stid, dtid); + + cubeb_stream_start(stream); + + pthread_join(subject, NULL); + pthread_join(detector, NULL); + + ASSERT_TRUE(called); + + fprintf(stderr, "\n%sDeadlock detected!\n", (called && !task_done.load()) ? "" : "No "); + + // Check the task is killed by ourselves if deadlock happends. + // Otherwise, thread_killer should not be triggered. + ASSERT_NE(task_done.load(), killed); + + ASSERT_TRUE(task_done.load()); + + cubeb_stream_stop(stream); +} + +#undef CALL_THREAD_KILLER diff --git a/media/libcubeb/test/test_device_changed_callback.cpp b/media/libcubeb/test/test_device_changed_callback.cpp new file mode 100644 index 0000000000..890ebd9e41 --- /dev/null +++ b/media/libcubeb/test/test_device_changed_callback.cpp @@ -0,0 +1,121 @@ +/* + * Copyright © 2018 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Check behaviors of registering device changed + * callbacks for the streams. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include <stdio.h> +#include <memory> +#include "cubeb/cubeb.h" + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE +#define INPUT_CHANNELS 1 +#define INPUT_LAYOUT CUBEB_LAYOUT_MONO +#define OUTPUT_CHANNELS 2 +#define OUTPUT_LAYOUT CUBEB_LAYOUT_STEREO + +long data_callback(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + return 0; +} + +void state_callback(cubeb_stream * stream, void * user, cubeb_state state) +{ +} + +void device_changed_callback(void * user) +{ + fprintf(stderr, "device changed callback\n"); + ASSERT_TRUE(false) << "Error: device changed callback" + " called without changing devices"; +} + +void test_registering_null_callback_twice(cubeb_stream * stream) +{ + int r = cubeb_stream_register_device_changed_callback(stream, nullptr); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + return; + } + ASSERT_EQ(r, CUBEB_OK) << "Error registering null device changed callback"; + + r = cubeb_stream_register_device_changed_callback(stream, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error registering null device changed callback again"; +} + +void test_registering_and_unregistering_callback(cubeb_stream * stream) +{ + int r = cubeb_stream_register_device_changed_callback(stream, device_changed_callback); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + return; + } + ASSERT_EQ(r, CUBEB_OK) << "Error registering device changed callback"; + + r = cubeb_stream_register_device_changed_callback(stream, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error unregistering device changed callback"; +} + +TEST(cubeb, device_changed_callbacks) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r = CUBEB_OK; + uint32_t latency_frames = 0; + + + r = common_init(&ctx, "Cubeb duplex example with device change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + if (!can_run_audio_input_test(ctx)) { + return; + } + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + /* typical user-case: mono input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = INPUT_LAYOUT; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", + NULL, &input_params, NULL, &output_params, + latency_frames, data_callback, state_callback, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + test_registering_null_callback_twice(stream); + + test_registering_and_unregistering_callback(stream); + + cubeb_stream_destroy(stream); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT +#undef INPUT_CHANNELS +#undef INPUT_LAYOUT +#undef OUTPUT_CHANNELS +#undef OUTPUT_LAYOUT diff --git a/media/libcubeb/test/test_devices.cpp b/media/libcubeb/test/test_devices.cpp new file mode 100644 index 0000000000..76e96159ee --- /dev/null +++ b/media/libcubeb/test/test_devices.cpp @@ -0,0 +1,262 @@ +/* + * Copyright © 2015 Haakon Sporsheim <haakon.sporsheim@telenordigital.com> + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb enumerate device test/example. + * Prints out a list of devices enumerated. */ +#include "gtest/gtest.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <memory> +#include "cubeb/cubeb.h" + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +long data_cb_duplex(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + // noop, unused + return 0; +} + +void state_cb_duplex(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + // noop, unused +} + +static void +print_device_info(cubeb_device_info * info, FILE * f) +{ + char devfmts[64] = ""; + const char * devtype, * devstate, * devdeffmt; + + switch (info->type) { + case CUBEB_DEVICE_TYPE_INPUT: + devtype = "input"; + break; + case CUBEB_DEVICE_TYPE_OUTPUT: + devtype = "output"; + break; + case CUBEB_DEVICE_TYPE_UNKNOWN: + default: + devtype = "unknown?"; + break; + }; + + switch (info->state) { + case CUBEB_DEVICE_STATE_DISABLED: + devstate = "disabled"; + break; + case CUBEB_DEVICE_STATE_UNPLUGGED: + devstate = "unplugged"; + break; + case CUBEB_DEVICE_STATE_ENABLED: + devstate = "enabled"; + break; + default: + devstate = "unknown?"; + break; + }; + + switch (info->default_format) { + case CUBEB_DEVICE_FMT_S16LE: + devdeffmt = "S16LE"; + break; + case CUBEB_DEVICE_FMT_S16BE: + devdeffmt = "S16BE"; + break; + case CUBEB_DEVICE_FMT_F32LE: + devdeffmt = "F32LE"; + break; + case CUBEB_DEVICE_FMT_F32BE: + devdeffmt = "F32BE"; + break; + default: + devdeffmt = "unknown?"; + break; + }; + + if (info->format & CUBEB_DEVICE_FMT_S16LE) + strcat(devfmts, " S16LE"); + if (info->format & CUBEB_DEVICE_FMT_S16BE) + strcat(devfmts, " S16BE"); + if (info->format & CUBEB_DEVICE_FMT_F32LE) + strcat(devfmts, " F32LE"); + if (info->format & CUBEB_DEVICE_FMT_F32BE) + strcat(devfmts, " F32BE"); + + fprintf(f, + "dev: \"%s\"%s\n" + "\tName: \"%s\"\n" + "\tGroup: \"%s\"\n" + "\tVendor: \"%s\"\n" + "\tType: %s\n" + "\tState: %s\n" + "\tCh: %u\n" + "\tFormat: %s (0x%x) (default: %s)\n" + "\tRate: %u - %u (default: %u)\n" + "\tLatency: lo %u frames, hi %u frames\n", + info->device_id, info->preferred ? " (PREFERRED)" : "", + info->friendly_name, info->group_id, info->vendor_name, + devtype, devstate, info->max_channels, + (devfmts[0] == '\0') ? devfmts : devfmts + 1, + (unsigned int)info->format, devdeffmt, + info->min_rate, info->max_rate, info->default_rate, + info->latency_lo, info->latency_hi); +} + +static void +print_device_collection(cubeb_device_collection * collection, FILE * f) +{ + uint32_t i; + + for (i = 0; i < collection->count; i++) + print_device_info(&collection->device[i], f); +} + +TEST(cubeb, destroy_default_collection) +{ + int r; + cubeb * ctx = NULL; + cubeb_device_collection collection{ nullptr, 0 }; + + r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + ASSERT_EQ(collection.device, nullptr); + ASSERT_EQ(collection.count, (size_t) 0); + + r = cubeb_device_collection_destroy(ctx, &collection); + if (r != CUBEB_ERROR_NOT_SUPPORTED) { + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(collection.device, nullptr); + ASSERT_EQ(collection.count, (size_t) 0); + } +} + +TEST(cubeb, enumerate_devices) +{ + int r; + cubeb * ctx = NULL; + cubeb_device_collection collection; + + r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + fprintf(stdout, "Enumerating input devices for backend %s\n", + cubeb_get_backend_id(ctx)); + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_INPUT, &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; + + fprintf(stdout, "Found %zu input devices\n", collection.count); + print_device_collection(&collection, stdout); + cubeb_device_collection_destroy(ctx, &collection); + + fprintf(stdout, "Enumerating output devices for backend %s\n", + cubeb_get_backend_id(ctx)); + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + + fprintf(stdout, "Found %zu output devices\n", collection.count); + print_device_collection(&collection, stdout); + cubeb_device_collection_destroy(ctx, &collection); + + uint32_t count_before_creating_duplex_stream; + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + count_before_creating_duplex_stream = collection.count; + cubeb_device_collection_destroy(ctx, &collection); + + if (!can_run_audio_input_test(ctx)) { + return; + } + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + input_params.format = output_params.format = CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = output_params.rate = 48000; + input_params.channels = output_params.channels = 1; + input_params.layout = output_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", + NULL, &input_params, NULL, &output_params, + 1024, data_cb_duplex, state_cb_duplex, nullptr); + + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + ASSERT_EQ(count_before_creating_duplex_stream, collection.count); + cubeb_device_collection_destroy(ctx, &collection); + + cubeb_stream_destroy(stream); +} + +TEST(cubeb, stream_get_current_device) +{ + cubeb * ctx = NULL; + int r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + fprintf(stdout, "Getting current devices for backend %s\n", + cubeb_get_backend_id(ctx)); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + cubeb_stream * stream = NULL; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + input_params.format = output_params.format = CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = output_params.rate = 48000; + input_params.channels = output_params.channels = 1; + input_params.layout = output_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", + NULL, &input_params, NULL, &output_params, + 1024, data_cb_duplex, state_cb_duplex, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_device * device; + r = cubeb_stream_get_current_device(stream, &device); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + fprintf(stderr, "Getting current device is not supported" + " for this backend, skipping this test.\n"); + return; + } + ASSERT_EQ(r, CUBEB_OK) << "Error getting current devices"; + + fprintf(stdout, "Current output device: %s\n", device->output_name); + fprintf(stdout, "Current input device: %s\n", device->input_name); + + r = cubeb_stream_device_destroy(stream, device); + ASSERT_EQ(r, CUBEB_OK) << "Error destroying current devices"; +} diff --git a/media/libcubeb/test/test_duplex.cpp b/media/libcubeb/test/test_duplex.cpp new file mode 100644 index 0000000000..1eb661e0e6 --- /dev/null +++ b/media/libcubeb/test/test_duplex.cpp @@ -0,0 +1,360 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Loops input back to output and check audio + * is flowing. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <memory> +#include "cubeb/cubeb.h" +#include <atomic> + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE +#define INPUT_CHANNELS 1 +#define INPUT_LAYOUT CUBEB_LAYOUT_MONO +#define OUTPUT_CHANNELS 2 +#define OUTPUT_LAYOUT CUBEB_LAYOUT_STEREO + +struct user_state_duplex +{ + std::atomic<int> invalid_audio_value{ 0 }; +}; + +long data_cb_duplex(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + user_state_duplex * u = reinterpret_cast<user_state_duplex*>(user); + float *ib = (float *)inputbuffer; + float *ob = (float *)outputbuffer; + + if (stream == NULL || inputbuffer == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + // Loop back: upmix the single input channel to the two output channels, + // checking if there is noise in the process. + long output_index = 0; + for (long i = 0; i < nframes; i++) { + if (ib[i] <= -1.0 || ib[i] >= 1.0) { + u->invalid_audio_value = 1; + break; + } + ob[output_index] = ob[output_index + 1] = ib[i]; + output_index += 2; + } + + return nframes; +} + +void state_cb_duplex(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; +} + +TEST(cubeb, duplex) +{ + cubeb *ctx; + cubeb_stream *stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + user_state_duplex stream_state; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb duplex example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + 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; + } + + /* typical user-case: mono input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = INPUT_LAYOUT; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", + NULL, &input_params, NULL, &output_params, + latency_frames, data_cb_duplex, state_cb_duplex, &stream_state); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(500); + cubeb_stream_stop(stream); + + ASSERT_FALSE(stream_state.invalid_audio_value.load()); +} + +void device_collection_changed_callback(cubeb * context, void * user) +{ + fprintf(stderr, "collection changed callback\n"); + ASSERT_TRUE(false) << "Error: device collection changed callback" + " called when opening a stream"; +} + +void +duplex_collection_change_impl(cubeb * ctx) +{ + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = cubeb_register_device_collection_changed( + ctx, static_cast<cubeb_device_type>(CUBEB_DEVICE_TYPE_INPUT), + device_collection_changed_callback, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + + /* typical user-case: mono input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = INPUT_LAYOUT; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, + &output_params, latency_frames, data_cb_duplex, + state_cb_duplex, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + cubeb_stream_destroy(stream); +} + +TEST(cubeb, duplex_collection_change) +{ + cubeb * ctx; + int r; + + r = common_init(&ctx, "Cubeb duplex example with collection change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> 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; + } + + duplex_collection_change_impl(ctx); + r = cubeb_register_device_collection_changed( + ctx, static_cast<cubeb_device_type>(CUBEB_DEVICE_TYPE_INPUT), nullptr, + nullptr); + ASSERT_EQ(r, CUBEB_OK); +} + +TEST(cubeb, duplex_collection_change_no_unregister) +{ + cubeb * ctx; + int r; + + r = common_init(&ctx, "Cubeb duplex example with collection change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + cubeb_destroy(ctx); + return; + } + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, [](cubeb * p) noexcept { EXPECT_DEATH(cubeb_destroy(p), ""); }); + + duplex_collection_change_impl(ctx); +} + +long data_cb_input(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + if (stream == NULL || inputbuffer == NULL || outputbuffer != NULL) { + return CUBEB_ERROR; + } + + return nframes; +} + +void state_cb_input(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; + case CUBEB_STATE_ERROR: + fprintf(stderr, "stream runs into error state\n"); break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +std::vector<cubeb_devid> get_devices(cubeb * ctx, cubeb_device_type type) { + std::vector<cubeb_devid> devices; + + cubeb_device_collection collection; + int r = cubeb_enumerate_devices(ctx, type, &collection); + + if (r != CUBEB_OK) { + fprintf(stderr, "Failed to enumerate devices\n"); + return devices; + } + + for (uint32_t i = 0; i < collection.count; i++) { + if (collection.device[i].state == CUBEB_DEVICE_STATE_ENABLED) { + devices.emplace_back(collection.device[i].devid); + } + } + + cubeb_device_collection_destroy(ctx, &collection); + + return devices; +} + +TEST(cubeb, one_duplex_one_input) +{ + cubeb *ctx; + cubeb_stream *duplex_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + user_state_duplex duplex_stream_state; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb duplex example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + /* This test needs at least two available input devices. */ + std::vector<cubeb_devid> input_devices = get_devices(ctx, CUBEB_DEVICE_TYPE_INPUT); + if (input_devices.size() < 2) { + return; + } + + /* This test needs at least one available output device. */ + std::vector<cubeb_devid> output_devices = get_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT); + if (output_devices.size() < 1) { + return; + } + + cubeb_devid duplex_input = input_devices.front(); + cubeb_devid duplex_output = nullptr; // default device + cubeb_devid input_only = input_devices.back(); + + /* typical use-case: mono voice input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = CUBEB_STREAM_PREF_VOICE; + + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &duplex_stream, "Cubeb duplex", + duplex_input, &input_params, duplex_output, &output_params, + latency_frames, data_cb_duplex, state_cb_duplex, &duplex_stream_state); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing duplex cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(duplex_stream, cubeb_stream_destroy); + + r = cubeb_stream_start(duplex_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not start duplex stream"; + delay(500); + + cubeb_stream *input_stream; + r = cubeb_stream_init(ctx, &input_stream, "Cubeb input", + input_only, &input_params, NULL, NULL, + latency_frames, data_cb_input, state_cb_input, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing input-only cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + r = cubeb_stream_start(input_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not start input stream"; + delay(500); + + r = cubeb_stream_stop(duplex_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not stop duplex stream"; + + r = cubeb_stream_stop(input_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not stop input stream"; + + ASSERT_FALSE(duplex_stream_state.invalid_audio_value.load()); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT +#undef INPUT_CHANNELS +#undef INPUT_LAYOUT +#undef OUTPUT_CHANNELS +#undef OUTPUT_LAYOUT diff --git a/media/libcubeb/test/test_latency.cpp b/media/libcubeb/test/test_latency.cpp new file mode 100644 index 0000000000..522851044a --- /dev/null +++ b/media/libcubeb/test/test_latency.cpp @@ -0,0 +1,47 @@ +#include "gtest/gtest.h" +#include <stdlib.h> +#include <memory> +#include "cubeb/cubeb.h" +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +TEST(cubeb, latency) +{ + cubeb * ctx = NULL; + int r; + uint32_t max_channels; + uint32_t preferred_rate; + uint32_t latency_frames; + + r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK); + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + r = cubeb_get_max_channel_count(ctx, &max_channels); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_GT(max_channels, 0u); + } + + r = cubeb_get_preferred_sample_rate(ctx, &preferred_rate); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_GT(preferred_rate, 0u); + } + + cubeb_stream_params params = { + CUBEB_SAMPLE_FLOAT32NE, + preferred_rate, + max_channels, + CUBEB_LAYOUT_UNDEFINED, + CUBEB_STREAM_PREF_NONE + }; + r = cubeb_get_min_latency(ctx, ¶ms, &latency_frames); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_GT(latency_frames, 0u); + } +} diff --git a/media/libcubeb/test/test_logging.cpp b/media/libcubeb/test/test_logging.cpp new file mode 100644 index 0000000000..5d9691d353 --- /dev/null +++ b/media/libcubeb/test/test_logging.cpp @@ -0,0 +1,193 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* cubeb_logging test */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include "cubeb_log.h" +#include <atomic> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> +#include <thread> + +#include "common.h" + +#define PRINT_LOGS_TO_STDERR 0 + +std::atomic<uint32_t> log_statements_received = {0}; +std::atomic<uint32_t> data_callback_call_count = {0}; + +void +test_logging_callback(char const * fmt, ...) +{ + log_statements_received++; +#if PRINT_LOGS_TO_STDERR == 1 + char buf[1024]; + va_list argslist; + va_start(argslist, fmt); + vsnprintf(buf, 1024, fmt, argslist); + fprintf(stderr, "%s\n", buf); + va_end(argslist); +#endif // PRINT_LOGS_TO_STDERR +} + +long +data_cb_load(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + data_callback_call_count++; + return nframes; +} + +void +state_cb(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; +} + +// Waits for at least one audio callback to have occured. +void +wait_for_audio_callback() +{ + uint32_t audio_callback_index = + data_callback_call_count.load(std::memory_order_acquire); + while (audio_callback_index == + data_callback_call_count.load(std::memory_order_acquire)) { + delay(100); + } +} + +TEST(cubeb, logging) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + cubeb_set_log_callback(CUBEB_LOG_NORMAL, test_logging_callback); + + r = common_init(&ctx, "Cubeb logging test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + output_params.format = CUBEB_SAMPLE_FLOAT32LE; + output_params.rate = 48000; + output_params.channels = 2; + output_params.layout = CUBEB_LAYOUT_STEREO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &stream, "Cubeb logging", NULL, NULL, NULL, + &output_params, latency_frames, data_cb_load, state_cb, + NULL); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + ASSERT_NE(log_statements_received.load(std::memory_order_acquire), 0u); + + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + log_statements_received.store(0, std::memory_order_release); + + // This is synchronous and we'll receive log messages on all backends that we + // test + cubeb_stream_start(stream); + + ASSERT_EQ(log_statements_received.load(std::memory_order_acquire), 0u); + + cubeb_set_log_callback(CUBEB_LOG_VERBOSE, test_logging_callback); + + wait_for_audio_callback(); + + ASSERT_NE(log_statements_received.load(std::memory_order_acquire), 0u); + + bool log_callback_set = true; + uint32_t iterations = 100; + while (iterations--) { + wait_for_audio_callback(); + + if (!log_callback_set) { + ASSERT_EQ(log_statements_received.load(std::memory_order_acquire), 0u); + // Set a logging callback, start logging + cubeb_set_log_callback(CUBEB_LOG_VERBOSE, test_logging_callback); + log_callback_set = true; + } else { + // Disable the logging callback, stop logging. + ASSERT_NE(log_statements_received.load(std::memory_order_acquire), 0u); + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + log_statements_received.store(0, std::memory_order_release); + // Disabling logging should flush any log message -- wait a bit and check + // that this is true. + ASSERT_EQ(log_statements_received.load(std::memory_order_acquire), 0u); + log_callback_set = false; + } + } + + cubeb_stream_stop(stream); +} + +TEST(cubeb, logging_stress) +{ + cubeb_set_log_callback(CUBEB_LOG_NORMAL, test_logging_callback); + + std::atomic<bool> thread_done = {false}; + + auto t = std::thread([&thread_done]() { + uint32_t count = 0; + do { + while (rand() % 10) { + ALOG("Log message #%d!", count++); + } + } while (count < 1e4); + thread_done.store(true); + }); + + bool enabled = true; + while (!thread_done.load()) { + if (enabled) { + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + enabled = false; + } else { + cubeb_set_log_callback(CUBEB_LOG_NORMAL, test_logging_callback); + enabled = true; + } + } + + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + + t.join(); + + ASSERT_TRUE(true); +} 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 <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <algorithm> +#include <memory> +#include <mutex> +#include <string> +#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<typename T> T ConvertSampleToOutput(double input); +template<> float ConvertSampleToOutput(double input) { return float(input); } +template<> short ConvertSampleToOutput(double input) { return short(input * 32767.0f); } + +template<typename T> 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<double> cross_correlate(std::vector<double> & f, + std::vector<double> & 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<double> 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<double> & output_frames, + std::vector<double> & input_frames, + size_t signal_length) +{ + std::vector<double> 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<double> normalize_frames(std::vector<double> & frames) { + double max = abs(*std::max_element(frames.begin(), frames.end(), + [](double a, double b) { return abs(a) < abs(b); })); + std::vector<double> 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<double> & output_frames, + std::vector<double> & input_frames) +{ + ASSERT_EQ(output_frames.size(), input_frames.size()) << "#Output frames != #input frames"; + size_t num_frames = output_frames.size(); + std::vector<double> normalized_output_frames = normalize_frames(output_frames); + std::vector<double> 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<double> output_frames; + /* track input */ + std::vector<double> input_frames; +}; + +template<typename T> +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<std::mutex> 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<T>(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<typename T> +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<std::mutex> lock(u->user_state_mutex); + for (int i = 0; i < nframes; i++) { + u->input_frames.push_back(ConvertSampleFromOutput(ib[i])); + } + + return nframes; +} + +template<typename T> +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<std::mutex> 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<T>(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<cubeb, decltype(&cubeb_destroy)> + 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_state_loopback> 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<float> : data_cb_loop_duplex<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + 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<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & output_frames = user_data->output_frames; + std::vector<double> & 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<double> 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<double> 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<cubeb, decltype(&cubeb_destroy)> + 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_state_loopback> 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<float> : data_cb_loop_input_only<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + 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<float> : data_cb_playback<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + 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<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & output_frames = user_data->output_frames; + std::vector<double> & 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<double> 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<double> 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<cubeb, decltype(&cubeb_destroy)> + 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_state_loopback> 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<float> : data_cb_loop_input_only<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + 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<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & 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<cubeb, decltype(&cubeb_destroy)> + 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_state_loopback> 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<float> : data_cb_loop_input_only<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + 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<float> : data_cb_playback<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + 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<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & output_frames = user_data->output_frames; + std::vector<double> & 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<double> 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<double> 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); +} diff --git a/media/libcubeb/test/test_overload_callback.cpp b/media/libcubeb/test/test_overload_callback.cpp new file mode 100644 index 0000000000..c8f4dafd17 --- /dev/null +++ b/media/libcubeb/test/test_overload_callback.cpp @@ -0,0 +1,101 @@ +/* + * Copyright © 2017 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <memory> +#include <atomic> +#include "cubeb/cubeb.h" +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_S16LE + +std::atomic<bool> load_callback{ false }; + +long data_cb(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + if (load_callback) { + fprintf(stderr, "Sleeping...\n"); + delay(100000); + fprintf(stderr, "Sleeping done\n"); + } + return nframes; +} + +void state_cb(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + ASSERT_TRUE(!!stream); + + 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: + FAIL() << "this test is not supposed to drain"; break; + case CUBEB_STATE_ERROR: + fprintf(stderr, "stream error\n"); break; + default: + FAIL() << "this test is not supposed to have a weird state"; break; + } +} + +TEST(cubeb, overload_callback) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb callback overload"); + ASSERT_EQ(r, CUBEB_OK); + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + // This test is specifically designed to test a behaviour of the WASAPI + // backend in a specific scenario. + if (strcmp(cubeb_get_backend_id(ctx), "wasapi") != 0) { + return; + } + + output_params.format = STREAM_FORMAT; + output_params.rate = 48000; + output_params.channels = 2; + output_params.layout = CUBEB_LAYOUT_STEREO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_init(ctx, &stream, "Cubeb", + NULL, NULL, NULL, &output_params, + latency_frames, data_cb, state_cb, NULL); + ASSERT_EQ(r, CUBEB_OK); + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(500); + // This causes the callback to sleep for a large number of seconds. + load_callback = true; + delay(500); + cubeb_stream_stop(stream); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_record.cpp b/media/libcubeb/test/test_record.cpp new file mode 100644 index 0000000000..ba0c758721 --- /dev/null +++ b/media/libcubeb/test/test_record.cpp @@ -0,0 +1,119 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Record the mic and check there is sound. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <memory> +#include "cubeb/cubeb.h" +#include <atomic> + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE + +struct user_state_record +{ + std::atomic<int> invalid_audio_value{ 0 }; +}; + +long data_cb_record(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + user_state_record * u = reinterpret_cast<user_state_record*>(user); + float *b = (float *)inputbuffer; + + if (stream == NULL || inputbuffer == NULL || outputbuffer != NULL) { + return CUBEB_ERROR; + } + + for (long i = 0; i < nframes; i++) { + if (b[i] <= -1.0 || b[i] >= 1.0) { + u->invalid_audio_value = 1; + break; + } + } + + return nframes; +} + +void state_cb_record(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; +} + +TEST(cubeb, record) +{ + if (cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr /*print_log*/) != CUBEB_OK) { + fprintf(stderr, "Set log callback failed\n"); + } + cubeb *ctx; + cubeb_stream *stream; + cubeb_stream_params params; + int r; + user_state_record stream_state; + + r = common_init(&ctx, "Cubeb record example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + 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; + } + + params.format = STREAM_FORMAT; + params.rate = SAMPLE_FREQUENCY; + params.channels = 1; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "Cubeb record (mono)", NULL, ¶ms, NULL, nullptr, + 4096, data_cb_record, state_cb_record, &stream_state); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(500); + cubeb_stream_stop(stream); + +#ifdef __linux__ + // user callback does not arrive in Linux, silence the error + fprintf(stderr, "Check is disabled in Linux\n"); +#else + ASSERT_FALSE(stream_state.invalid_audio_value.load()); +#endif +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_resampler.cpp b/media/libcubeb/test/test_resampler.cpp new file mode 100644 index 0000000000..3d55db66ed --- /dev/null +++ b/media/libcubeb/test/test_resampler.cpp @@ -0,0 +1,1089 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX +#include "gtest/gtest.h" +#include "common.h" +#include "cubeb_resampler_internal.h" +#include <stdio.h> +#include <algorithm> +#include <iostream> + +/* Windows cmath USE_MATH_DEFINE thing... */ +const float PI = 3.14159265359f; + +/* Testing all sample rates is very long, so if THOROUGH_TESTING is not defined, + * only part of the test suite is ran. */ +#ifdef THOROUGH_TESTING +/* Some standard sample rates we're testing with. */ +const uint32_t sample_rates[] = { + 8000, + 16000, + 32000, + 44100, + 48000, + 88200, + 96000, + 192000 +}; +/* The maximum number of channels we're resampling. */ +const uint32_t max_channels = 2; +/* The minimum an maximum number of milliseconds we're resampling for. This is + * used to simulate the fact that the audio stream is resampled in chunks, + * because audio is delivered using callbacks. */ +const uint32_t min_chunks = 10; /* ms */ +const uint32_t max_chunks = 30; /* ms */ +const uint32_t chunk_increment = 1; + +#else + +const uint32_t sample_rates[] = { + 8000, + 44100, + 48000, +}; +const uint32_t max_channels = 2; +const uint32_t min_chunks = 10; /* ms */ +const uint32_t max_chunks = 30; /* ms */ +const uint32_t chunk_increment = 10; +#endif + +// #define DUMP_ARRAYS +#ifdef DUMP_ARRAYS +/** + * Files produced by dump(...) can be converted to .wave files using: + * + * sox -c <channel_count> -r <rate> -e float -b 32 file.raw file.wav + * + * for floating-point audio, or: + * + * sox -c <channel_count> -r <rate> -e unsigned -b 16 file.raw file.wav + * + * for 16bit integer audio. + */ + +/* Use the correct implementation of fopen, depending on the platform. */ +void fopen_portable(FILE ** f, const char * name, const char * mode) +{ +#ifdef WIN32 + fopen_s(f, name, mode); +#else + *f = fopen(name, mode); +#endif +} + +template<typename T> +void dump(const char * name, T * frames, size_t count) +{ + FILE * file; + fopen_portable(&file, name, "wb"); + + if (!file) { + fprintf(stderr, "error opening %s\n", name); + return; + } + + if (count != fwrite(frames, sizeof(T), count, file)) { + fprintf(stderr, "error writing to %s\n", name); + } + fclose(file); +} +#else +template<typename T> +void dump(const char * name, T * frames, size_t count) +{ } +#endif + +// The more the ratio is far from 1, the more we accept a big error. +float epsilon_tweak_ratio(float ratio) +{ + return ratio >= 1 ? ratio : 1 / ratio; +} + +// Epsilon values for comparing resampled data to expected data. +// The bigger the resampling ratio is, the more lax we are about errors. +template<typename T> +T epsilon(float ratio); + +template<> +float epsilon(float ratio) { + return 0.08f * epsilon_tweak_ratio(ratio); +} + +template<> +int16_t epsilon(float ratio) { + return static_cast<int16_t>(10 * epsilon_tweak_ratio(ratio)); +} + +void test_delay_lines(uint32_t delay_frames, uint32_t channels, uint32_t chunk_ms) +{ + const size_t length_s = 2; + const size_t rate = 44100; + const size_t length_frames = rate * length_s; + delay_line<float> delay(delay_frames, channels, rate); + auto_array<float> input; + auto_array<float> output; + uint32_t chunk_length = channels * chunk_ms * rate / 1000; + uint32_t output_offset = 0; + uint32_t channel = 0; + + /** Generate diracs every 100 frames, and check they are delayed. */ + input.push_silence(length_frames * channels); + for (uint32_t i = 0; i < input.length() - 1; i+=100) { + input.data()[i + channel] = 0.5; + channel = (channel + 1) % channels; + } + dump("input.raw", input.data(), input.length()); + while(input.length()) { + uint32_t to_pop = std::min<uint32_t>(input.length(), chunk_length * channels); + float * in = delay.input_buffer(to_pop / channels); + input.pop(in, to_pop); + delay.written(to_pop / channels); + output.push_silence(to_pop); + delay.output(output.data() + output_offset, to_pop / channels); + output_offset += to_pop; + } + + // Check the diracs have been shifted by `delay_frames` frames. + for (uint32_t i = 0; i < output.length() - delay_frames * channels + 1; i+=100) { + ASSERT_EQ(output.data()[i + channel + delay_frames * channels], 0.5); + channel = (channel + 1) % channels; + } + + dump("output.raw", output.data(), output.length()); +} +/** + * This takes sine waves with a certain `channels` count, `source_rate`, and + * resample them, by chunk of `chunk_duration` milliseconds, to `target_rate`. + * Then a sample-wise comparison is performed against a sine wave generated at + * the correct rate. + */ +template<typename T> +void test_resampler_one_way(uint32_t channels, uint32_t source_rate, uint32_t target_rate, float chunk_duration) +{ + size_t chunk_duration_in_source_frames = static_cast<uint32_t>(ceil(chunk_duration * source_rate / 1000.)); + float resampling_ratio = static_cast<float>(source_rate) / target_rate; + cubeb_resampler_speex_one_way<T> resampler(channels, source_rate, target_rate, 3); + auto_array<T> source(channels * source_rate * 10); + auto_array<T> destination(channels * target_rate * 10); + auto_array<T> expected(channels * target_rate * 10); + uint32_t phase_index = 0; + uint32_t offset = 0; + const uint32_t buf_len = 2; /* seconds */ + + // generate a sine wave in each channel, at the source sample rate + source.push_silence(channels * source_rate * buf_len); + while(offset != source.length()) { + float p = phase_index++ / static_cast<float>(source_rate); + for (uint32_t j = 0; j < channels; j++) { + source.data()[offset++] = 0.5 * sin(440. * 2 * PI * p); + } + } + + dump("input.raw", source.data(), source.length()); + + expected.push_silence(channels * target_rate * buf_len); + // generate a sine wave in each channel, at the target sample rate. + // Insert silent samples at the beginning to account for the resampler latency. + offset = resampler.latency() * channels; + for (uint32_t i = 0; i < offset; i++) { + expected.data()[i] = 0.0f; + } + phase_index = 0; + while (offset != expected.length()) { + float p = phase_index++ / static_cast<float>(target_rate); + for (uint32_t j = 0; j < channels; j++) { + expected.data()[offset++] = 0.5 * sin(440. * 2 * PI * p); + } + } + + dump("expected.raw", expected.data(), expected.length()); + + // resample by chunk + uint32_t write_offset = 0; + destination.push_silence(channels * target_rate * buf_len); + while (write_offset < destination.length()) + { + size_t output_frames = static_cast<uint32_t>(floor(chunk_duration_in_source_frames / resampling_ratio)); + uint32_t input_frames = resampler.input_needed_for_output(output_frames); + resampler.input(source.data(), input_frames); + source.pop(nullptr, input_frames * channels); + resampler.output(destination.data() + write_offset, + std::min(output_frames, (destination.length() - write_offset) / channels)); + write_offset += output_frames * channels; + } + + dump("output.raw", destination.data(), expected.length()); + + // compare, taking the latency into account + bool fuzzy_equal = true; + for (uint32_t i = resampler.latency() + 1; i < expected.length(); i++) { + float diff = fabs(expected.data()[i] - destination.data()[i]); + if (diff > epsilon<T>(resampling_ratio)) { + fprintf(stderr, "divergence at %d: %f %f (delta %f)\n", i, expected.data()[i], destination.data()[i], diff); + fuzzy_equal = false; + } + } + ASSERT_TRUE(fuzzy_equal); +} + +template<typename T> +cubeb_sample_format cubeb_format(); + +template<> +cubeb_sample_format cubeb_format<float>() +{ + return CUBEB_SAMPLE_FLOAT32NE; +} + +template<> +cubeb_sample_format cubeb_format<short>() +{ + return CUBEB_SAMPLE_S16NE; +} + +struct osc_state { + osc_state() + : input_phase_index(0) + , output_phase_index(0) + , output_offset(0) + , input_channels(0) + , output_channels(0) + {} + uint32_t input_phase_index; + uint32_t max_output_phase_index; + uint32_t output_phase_index; + uint32_t output_offset; + uint32_t input_channels; + uint32_t output_channels; + uint32_t output_rate; + uint32_t target_rate; + auto_array<float> input; + auto_array<float> output; +}; + +uint32_t fill_with_sine(float * buf, uint32_t rate, uint32_t channels, + uint32_t frames, uint32_t initial_phase) +{ + uint32_t offset = 0; + for (uint32_t i = 0; i < frames; i++) { + float p = initial_phase++ / static_cast<float>(rate); + for (uint32_t j = 0; j < channels; j++) { + buf[offset++] = 0.5 * sin(440. * 2 * PI * p); + } + } + return initial_phase; +} + +long data_cb_resampler(cubeb_stream * /*stm*/, void * user_ptr, + const void * input_buffer, void * output_buffer, long frame_count) +{ + osc_state * state = reinterpret_cast<osc_state*>(user_ptr); + const float * in = reinterpret_cast<const float*>(input_buffer); + float * out = reinterpret_cast<float*>(output_buffer); + + state->input.push(in, frame_count * state->input_channels); + + /* Check how much output frames we need to write */ + uint32_t remaining = state->max_output_phase_index - state->output_phase_index; + uint32_t to_write = std::min<uint32_t>(remaining, frame_count); + state->output_phase_index = fill_with_sine(out, + state->target_rate, + state->output_channels, + to_write, + state->output_phase_index); + + return to_write; +} + +template<typename T> +bool array_fuzzy_equal(const auto_array<T>& lhs, const auto_array<T>& rhs, T epsi) +{ + uint32_t len = std::min(lhs.length(), rhs.length()); + + for (uint32_t i = 0; i < len; i++) { + if (fabs(lhs.at(i) - rhs.at(i)) > epsi) { + std::cout << "not fuzzy equal at index: " << i + << " lhs: " << lhs.at(i) << " rhs: " << rhs.at(i) + << " delta: " << fabs(lhs.at(i) - rhs.at(i)) + << " epsilon: "<< epsi << std::endl; + return false; + } + } + return true; +} + +template<typename T> +void test_resampler_duplex(uint32_t input_channels, uint32_t output_channels, + uint32_t input_rate, uint32_t output_rate, + uint32_t target_rate, float chunk_duration) +{ + cubeb_stream_params input_params; + cubeb_stream_params output_params; + osc_state state; + + input_params.format = output_params.format = cubeb_format<T>(); + state.input_channels = input_params.channels = input_channels; + state.output_channels = output_params.channels = output_channels; + input_params.rate = input_rate; + state.output_rate = output_params.rate = output_rate; + state.target_rate = target_rate; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; + long got; + + cubeb_resampler * resampler = + cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, target_rate, + data_cb_resampler, (void*)&state, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + long latency = cubeb_resampler_latency(resampler); + + const uint32_t duration_s = 2; + int32_t duration_frames = duration_s * target_rate; + uint32_t input_array_frame_count = ceil(chunk_duration * input_rate / 1000) + ceilf(static_cast<float>(input_rate) / target_rate) * 2; + uint32_t output_array_frame_count = chunk_duration * output_rate / 1000; + auto_array<float> input_buffer(input_channels * input_array_frame_count); + auto_array<float> output_buffer(output_channels * output_array_frame_count); + auto_array<float> expected_resampled_input(input_channels * duration_frames); + auto_array<float> expected_resampled_output(output_channels * output_rate * duration_s); + + state.max_output_phase_index = duration_s * target_rate; + + expected_resampled_input.push_silence(input_channels * duration_frames); + expected_resampled_output.push_silence(output_channels * output_rate * duration_s); + + /* expected output is a 440Hz sine wave at 16kHz */ + fill_with_sine(expected_resampled_input.data() + latency, + target_rate, input_channels, duration_frames - latency, 0); + /* expected output is a 440Hz sine wave at 32kHz */ + fill_with_sine(expected_resampled_output.data() + latency, + output_rate, output_channels, output_rate * duration_s - latency, 0); + + while (state.output_phase_index != state.max_output_phase_index) { + uint32_t leftover_samples = input_buffer.length() * input_channels; + input_buffer.reserve(input_array_frame_count); + state.input_phase_index = fill_with_sine(input_buffer.data() + leftover_samples, + input_rate, + input_channels, + input_array_frame_count - leftover_samples, + state.input_phase_index); + long input_consumed = input_array_frame_count; + input_buffer.set_length(input_array_frame_count); + + got = cubeb_resampler_fill(resampler, + input_buffer.data(), &input_consumed, + output_buffer.data(), output_array_frame_count); + + /* handle leftover input */ + if (input_array_frame_count != static_cast<uint32_t>(input_consumed)) { + input_buffer.pop(nullptr, input_consumed * input_channels); + } else { + input_buffer.clear(); + } + + state.output.push(output_buffer.data(), got * state.output_channels); + } + + dump("input_expected.raw", expected_resampled_input.data(), expected_resampled_input.length()); + dump("output_expected.raw", expected_resampled_output.data(), expected_resampled_output.length()); + dump("input.raw", state.input.data(), state.input.length()); + dump("output.raw", state.output.data(), state.output.length()); + + // This is disabled because the latency estimation in the resampler code is + // slightly off so we can generate expected vectors. + // See https://github.com/kinetiknz/cubeb/issues/93 + // ASSERT_TRUE(array_fuzzy_equal(state.input, expected_resampled_input, epsilon<T>(input_rate/target_rate))); + // ASSERT_TRUE(array_fuzzy_equal(state.output, expected_resampled_output, epsilon<T>(output_rate/target_rate))); + + cubeb_resampler_destroy(resampler); +} + +#define array_size(x) (sizeof(x) / sizeof(x[0])) + +TEST(cubeb, resampler_one_way) +{ + /* Test one way resamplers */ + for (uint32_t channels = 1; channels <= max_channels; channels++) { + for (uint32_t source_rate = 0; source_rate < array_size(sample_rates); source_rate++) { + for (uint32_t dest_rate = 0; dest_rate < array_size(sample_rates); dest_rate++) { + for (uint32_t chunk_duration = min_chunks; chunk_duration < max_chunks; chunk_duration+=chunk_increment) { + fprintf(stderr, "one_way: channels: %d, source_rate: %d, dest_rate: %d, chunk_duration: %d\n", + channels, sample_rates[source_rate], sample_rates[dest_rate], chunk_duration); + test_resampler_one_way<float>(channels, sample_rates[source_rate], + sample_rates[dest_rate], chunk_duration); + } + } + } + } +} + +TEST(cubeb, DISABLED_resampler_duplex) +{ + for (uint32_t input_channels = 1; input_channels <= max_channels; input_channels++) { + for (uint32_t output_channels = 1; output_channels <= max_channels; output_channels++) { + for (uint32_t source_rate_input = 0; source_rate_input < array_size(sample_rates); source_rate_input++) { + for (uint32_t source_rate_output = 0; source_rate_output < array_size(sample_rates); source_rate_output++) { + for (uint32_t dest_rate = 0; dest_rate < array_size(sample_rates); dest_rate++) { + for (uint32_t chunk_duration = min_chunks; chunk_duration < max_chunks; chunk_duration+=chunk_increment) { + fprintf(stderr, "input channels:%d output_channels:%d input_rate:%d " + "output_rate:%d target_rate:%d chunk_ms:%d\n", + input_channels, output_channels, + sample_rates[source_rate_input], + sample_rates[source_rate_output], + sample_rates[dest_rate], + chunk_duration); + test_resampler_duplex<float>(input_channels, output_channels, + sample_rates[source_rate_input], + sample_rates[source_rate_output], + sample_rates[dest_rate], + chunk_duration); + } + } + } + } + } + } +} + +TEST(cubeb, resampler_delay_line) +{ + for (uint32_t channel = 1; channel <= 2; channel++) { + for (uint32_t delay_frames = 4; delay_frames <= 40; delay_frames+=chunk_increment) { + for (uint32_t chunk_size = 10; chunk_size <= 30; chunk_size++) { + fprintf(stderr, "channel: %d, delay_frames: %d, chunk_size: %d\n", + channel, delay_frames, chunk_size); + test_delay_lines(delay_frames, channel, chunk_size); + } + } + } +} + +long test_output_only_noop_data_cb(cubeb_stream * /*stm*/, void * /*user_ptr*/, + const void * input_buffer, + void * output_buffer, long frame_count) +{ + EXPECT_TRUE(output_buffer); + EXPECT_TRUE(!input_buffer); + return frame_count; +} + +TEST(cubeb, resampler_output_only_noop) +{ + cubeb_stream_params output_params; + int target_rate; + + output_params.rate = 44100; + output_params.channels = 1; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + target_rate = output_params.rate; + + cubeb_resampler * resampler = + cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, target_rate, + test_output_only_noop_data_cb, nullptr, + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + const long out_frames = 128; + float out_buffer[out_frames]; + long got; + + got = cubeb_resampler_fill(resampler, nullptr, nullptr, + out_buffer, out_frames); + + ASSERT_EQ(got, out_frames); + + cubeb_resampler_destroy(resampler); +} + +long test_drain_data_cb(cubeb_stream * /*stm*/, void * user_ptr, + const void * input_buffer, + void * output_buffer, long frame_count) +{ + EXPECT_TRUE(output_buffer); + EXPECT_TRUE(!input_buffer); + auto cb_count = static_cast<int *>(user_ptr); + (*cb_count)++; + return frame_count - 1; +} + +TEST(cubeb, resampler_drain) +{ + cubeb_stream_params output_params; + int target_rate; + + output_params.rate = 44100; + output_params.channels = 1; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + target_rate = 48000; + int cb_count = 0; + + cubeb_resampler * resampler = + cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, target_rate, + test_drain_data_cb, &cb_count, + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + const long out_frames = 128; + float out_buffer[out_frames]; + long got; + + do { + got = cubeb_resampler_fill(resampler, nullptr, nullptr, + out_buffer, out_frames); + } while (got == out_frames); + + /* The callback should be called once but not again after returning < + * frame_count. */ + ASSERT_EQ(cb_count, 1); + + cubeb_resampler_destroy(resampler); +} + +// gtest does not support using ASSERT_EQ and friend in a function that returns +// a value. +void check_output(const void * input_buffer, void * output_buffer, long frame_count) +{ + ASSERT_EQ(input_buffer, nullptr); + ASSERT_EQ(frame_count, 256); + ASSERT_TRUE(!!output_buffer); +} + +long cb_passthrough_resampler_output(cubeb_stream * /*stm*/, void * /*user_ptr*/, + const void * input_buffer, + void * output_buffer, long frame_count) +{ + check_output(input_buffer, output_buffer, frame_count); + return frame_count; +} + +TEST(cubeb, resampler_passthrough_output_only) +{ + // Test that the passthrough resampler works when there is only an output stream. + cubeb_stream_params output_params; + + const size_t output_channels = 2; + output_params.channels = output_channels; + output_params.rate = 44100; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + int target_rate = output_params.rate; + + cubeb_resampler * resampler = + cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, + target_rate, cb_passthrough_resampler_output, nullptr, + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + float output_buffer[output_channels * 256]; + + long got; + for (uint32_t i = 0; i < 30; i++) { + got = cubeb_resampler_fill(resampler, nullptr, nullptr, output_buffer, 256); + ASSERT_EQ(got, 256); + } + + cubeb_resampler_destroy(resampler); +} + +// gtest does not support using ASSERT_EQ and friend in a function that returns +// a value. +void check_input(const void * input_buffer, void * output_buffer, long frame_count) +{ + ASSERT_EQ(output_buffer, nullptr); + ASSERT_EQ(frame_count, 256); + ASSERT_TRUE(!!input_buffer); +} + +long cb_passthrough_resampler_input(cubeb_stream * /*stm*/, void * /*user_ptr*/, + const void * input_buffer, + void * output_buffer, long frame_count) +{ + check_input(input_buffer, output_buffer, frame_count); + return frame_count; +} + +TEST(cubeb, resampler_passthrough_input_only) +{ + // Test that the passthrough resampler works when there is only an output stream. + cubeb_stream_params input_params; + + const size_t input_channels = 2; + input_params.channels = input_channels; + input_params.rate = 44100; + input_params.format = CUBEB_SAMPLE_FLOAT32NE; + int target_rate = input_params.rate; + + cubeb_resampler * resampler = + cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, nullptr, + target_rate, cb_passthrough_resampler_input, nullptr, + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + float input_buffer[input_channels * 256]; + + long got; + for (uint32_t i = 0; i < 30; i++) { + long int frames = 256; + got = cubeb_resampler_fill(resampler, input_buffer, &frames, nullptr, 0); + ASSERT_EQ(got, 256); + } + + cubeb_resampler_destroy(resampler); +} + +template<typename T> +long seq(T* array, int stride, long start, long count) +{ + uint32_t output_idx = 0; + for(int i = 0; i < count; i++) { + for (int j = 0; j < stride; j++) { + array[output_idx + j] = static_cast<T>(start + i); + } + output_idx += stride; + } + return start + count; +} + +template<typename T> +void is_seq(T * array, int stride, long count, long expected_start) +{ + uint32_t output_index = 0; + for (long i = 0; i < count; i++) { + for (int j = 0; j < stride; j++) { + ASSERT_EQ(array[output_index + j], expected_start + i); + } + output_index += stride; + } +} + +template<typename T> +void is_not_seq(T * array, int stride, long count, long expected_start) +{ + uint32_t output_index = 0; + for (long i = 0; i < count; i++) { + for (int j = 0; j < stride; j++) { + ASSERT_NE(array[output_index + j], expected_start + i); + } + output_index += stride; + } +} + +struct closure { + int input_channel_count; +}; + +// gtest does not support using ASSERT_EQ and friend in a function that returns +// a value. +template<typename T> +void check_duplex(const T * input_buffer, + T * output_buffer, long frame_count, + int input_channel_count) +{ + ASSERT_EQ(frame_count, 256); + // Silence scan-build warning. + ASSERT_TRUE(!!output_buffer); assert(output_buffer); + ASSERT_TRUE(!!input_buffer); assert(input_buffer); + + int output_index = 0; + int input_index = 0; + for (int i = 0; i < frame_count; i++) { + // output is two channels, input one or two channels. + if (input_channel_count == 1) { + output_buffer[output_index] = output_buffer[output_index + 1] = input_buffer[i]; + } else if (input_channel_count == 2) { + output_buffer[output_index] = input_buffer[input_index]; + output_buffer[output_index + 1] = input_buffer[input_index + 1]; + } + output_index += 2; + input_index += input_channel_count; + } +} + +long cb_passthrough_resampler_duplex(cubeb_stream * /*stm*/, void * user_ptr, + const void * input_buffer, + void * output_buffer, long frame_count) +{ + closure * c = reinterpret_cast<closure*>(user_ptr); + check_duplex<float>(static_cast<const float*>(input_buffer), + static_cast<float*>(output_buffer), + frame_count, c->input_channel_count); + return frame_count; +} + + +TEST(cubeb, resampler_passthrough_duplex_callback_reordering) +{ + // Test that when pre-buffering on resampler creation, we can survive an input + // callback being delayed. + + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + const int input_channels = 1; + const int output_channels = 2; + + input_params.channels = input_channels; + input_params.rate = 44100; + input_params.format = CUBEB_SAMPLE_FLOAT32NE; + + output_params.channels = output_channels; + output_params.rate = input_params.rate; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + + int target_rate = input_params.rate; + + closure c; + c.input_channel_count = input_channels; + + cubeb_resampler * resampler = + cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, + target_rate, cb_passthrough_resampler_duplex, &c, + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + const long BUF_BASE_SIZE = 256; + float input_buffer_prebuffer[input_channels * BUF_BASE_SIZE * 2]; + float input_buffer_glitch[input_channels * BUF_BASE_SIZE * 2]; + float input_buffer_normal[input_channels * BUF_BASE_SIZE]; + float output_buffer[output_channels * BUF_BASE_SIZE]; + + long seq_idx = 0; + long output_seq_idx = 0; + + long prebuffer_frames = ARRAY_LENGTH(input_buffer_prebuffer) / input_params.channels; + seq_idx = seq(input_buffer_prebuffer, input_channels, seq_idx, + prebuffer_frames); + + long got = cubeb_resampler_fill(resampler, input_buffer_prebuffer, &prebuffer_frames, + output_buffer, BUF_BASE_SIZE); + + output_seq_idx += BUF_BASE_SIZE; + + // prebuffer_frames will hold the frames used by the resampler. + ASSERT_EQ(prebuffer_frames, BUF_BASE_SIZE); + ASSERT_EQ(got, BUF_BASE_SIZE); + + for (uint32_t i = 0; i < 300; i++) { + long int frames = BUF_BASE_SIZE; + // Simulate that sometimes, we don't have the input callback on time + if (i != 0 && (i % 100) == 0) { + long zero = 0; + got = cubeb_resampler_fill(resampler, input_buffer_normal /* unused here */, + &zero, output_buffer, BUF_BASE_SIZE); + is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } else if (i != 0 && (i % 100) == 1) { + // if this is the case, the on the next iteration, we'll have twice the + // amount of input frames + seq_idx = seq(input_buffer_glitch, input_channels, seq_idx, BUF_BASE_SIZE * 2); + frames = 2 * BUF_BASE_SIZE; + got = cubeb_resampler_fill(resampler, input_buffer_glitch, &frames, output_buffer, BUF_BASE_SIZE); + is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } else { + // normal case + seq_idx = seq(input_buffer_normal, input_channels, seq_idx, BUF_BASE_SIZE); + long normal_input_frame_count = 256; + got = cubeb_resampler_fill(resampler, input_buffer_normal, &normal_input_frame_count, output_buffer, BUF_BASE_SIZE); + is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } + ASSERT_EQ(got, BUF_BASE_SIZE); + } + + cubeb_resampler_destroy(resampler); +} + +// Artificially simulate output thread underruns, +// by building up artificial delay in the input. +// Check that the frame drop logic kicks in. +TEST(cubeb, resampler_drift_drop_data) +{ + for (uint32_t input_channels = 1; input_channels < 3; input_channels++) { + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + const int output_channels = 2; + const int sample_rate = 44100; + + input_params.channels = input_channels; + input_params.rate = sample_rate; + input_params.format = CUBEB_SAMPLE_FLOAT32NE; + + output_params.channels = output_channels; + output_params.rate = sample_rate; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + + int target_rate = input_params.rate; + + closure c; + c.input_channel_count = input_channels; + + cubeb_resampler * resampler = + cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, + target_rate, cb_passthrough_resampler_duplex, &c, + CUBEB_RESAMPLER_QUALITY_VOIP, CUBEB_RESAMPLER_RECLOCK_NONE); + + const long BUF_BASE_SIZE = 256; + + // The factor by which the deadline is missed. This is intentionally + // kind of large to trigger the frame drop quickly. In real life, multiple + // smaller under-runs would accumulate. + const long UNDERRUN_FACTOR = 10; + // Number buffer used for pre-buffering, that some backends do. + const long PREBUFFER_FACTOR = 2; + + std::vector<float> input_buffer_prebuffer(input_channels * BUF_BASE_SIZE * PREBUFFER_FACTOR); + std::vector<float> input_buffer_glitch(input_channels * BUF_BASE_SIZE * UNDERRUN_FACTOR); + std::vector<float> input_buffer_normal(input_channels * BUF_BASE_SIZE); + std::vector<float> output_buffer(output_channels * BUF_BASE_SIZE); + + long seq_idx = 0; + long output_seq_idx = 0; + + long prebuffer_frames = input_buffer_prebuffer.size() / input_params.channels; + seq_idx = seq(input_buffer_prebuffer.data(), input_channels, seq_idx, + prebuffer_frames); + + long got = cubeb_resampler_fill(resampler, input_buffer_prebuffer.data(), &prebuffer_frames, + output_buffer.data(), BUF_BASE_SIZE); + + output_seq_idx += BUF_BASE_SIZE; + + // prebuffer_frames will hold the frames used by the resampler. + ASSERT_EQ(prebuffer_frames, BUF_BASE_SIZE); + ASSERT_EQ(got, BUF_BASE_SIZE); + + for (uint32_t i = 0; i < 300; i++) { + long int frames = BUF_BASE_SIZE; + if (i != 0 && (i % 100) == 1) { + // Once in a while, the output thread misses its deadline. + // The input thread still produces data, so it ends up accumulating. Simulate this by providing a + // much bigger input buffer. Check that the sequence is now unaligned, meaning we've dropped data + // to keep everything in sync. + seq_idx = seq(input_buffer_glitch.data(), input_channels, seq_idx, BUF_BASE_SIZE * UNDERRUN_FACTOR); + frames = BUF_BASE_SIZE * UNDERRUN_FACTOR; + got = cubeb_resampler_fill(resampler, input_buffer_glitch.data(), &frames, output_buffer.data(), BUF_BASE_SIZE); + is_seq(output_buffer.data(), 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } + else if (i != 0 && (i % 100) == 2) { + // On the next iteration, the sequence should be broken + seq_idx = seq(input_buffer_normal.data(), input_channels, seq_idx, BUF_BASE_SIZE); + long normal_input_frame_count = 256; + got = cubeb_resampler_fill(resampler, input_buffer_normal.data(), &normal_input_frame_count, output_buffer.data(), BUF_BASE_SIZE); + is_not_seq(output_buffer.data(), output_channels, BUF_BASE_SIZE, output_seq_idx); + // Reclock so that we can use is_seq again. + output_seq_idx = output_buffer[BUF_BASE_SIZE * output_channels - 1] + 1; + } + else { + // normal case + seq_idx = seq(input_buffer_normal.data(), input_channels, seq_idx, BUF_BASE_SIZE); + long normal_input_frame_count = 256; + got = cubeb_resampler_fill(resampler, input_buffer_normal.data(), &normal_input_frame_count, output_buffer.data(), BUF_BASE_SIZE); + is_seq(output_buffer.data(), output_channels, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } + ASSERT_EQ(got, BUF_BASE_SIZE); + } + + cubeb_resampler_destroy(resampler); + } +} + +static long +passthrough_resampler_fill_eq_input(cubeb_stream * stream, + void * user_ptr, + void const * input_buffer, + void * output_buffer, + long nframes) { + // gtest does not support using ASSERT_EQ and friends in a + // function that returns a value. + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float* input = static_cast<const float*>(input_buffer); + for (int i = 0; i < 64; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.01 * i); + } + }(); + return nframes; +} + +TEST(cubeb, passthrough_resampler_fill_eq_input) { + uint32_t channels = 2; + uint32_t sample_rate = 44100; + passthrough_resampler<float> resampler = + passthrough_resampler<float>(nullptr, passthrough_resampler_fill_eq_input, + nullptr, channels, sample_rate); + + long input_frame_count = 32; + long output_frame_count = 32; + float input[64] = {}; + float output[64] = {}; + for (uint32_t i = 0; i < input_frame_count * channels; ++i) { + input[i] = 0.01 * i; + } + long got = resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used must be equal to output frames. + ASSERT_EQ(input_frame_count, output_frame_count); +} + +static long +passthrough_resampler_fill_short_input(cubeb_stream * stream, + void * user_ptr, + void const * input_buffer, + void * output_buffer, + long nframes) { + // gtest does not support using ASSERT_EQ and friends in a + // function that returns a value. + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float* input = static_cast<const float*>(input_buffer); + // First part contains the input + for (int i = 0; i < 32; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.01 * i); + } + // missing part contains silence + for (int i = 32; i < 64; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.0); + } + }(); + return nframes; +} + +TEST(cubeb, passthrough_resampler_fill_short_input) { + uint32_t channels = 2; + uint32_t sample_rate = 44100; + passthrough_resampler<float> resampler = + passthrough_resampler<float>(nullptr, passthrough_resampler_fill_short_input, + nullptr, channels, sample_rate); + + long input_frame_count = 16; + long output_frame_count = 32; + float input[64] = {}; + float output[64] = {}; + for (uint32_t i = 0; i < input_frame_count * channels; ++i) { + input[i] = 0.01 * i; + } + long got = resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used are less than the output frames due to glitch. + ASSERT_EQ(input_frame_count, output_frame_count - 16); +} + +static long +passthrough_resampler_fill_input_left(cubeb_stream * stream, + void * user_ptr, + void const * input_buffer, + void * output_buffer, + long nframes) { + // gtest does not support using ASSERT_EQ and friends in a + // function that returns a value. + int iteration = *static_cast<int*>(user_ptr); + if (iteration == 1) { + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float* input = static_cast<const float*>(input_buffer); + for (int i = 0; i < 64; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.01 * i); + } + }(); + } else if (iteration == 2) { + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float* input = static_cast<const float*>(input_buffer); + for (int i = 0; i < 32; ++i) { + // First part contains the reamaining input samples from previous + // iteration (since they were more). + ASSERT_FLOAT_EQ(input[i], 0.01 * (i + 64)); + // next part contains the new buffer + ASSERT_FLOAT_EQ(input[i + 32], 0.01 * i); + } + }(); + } else if (iteration == 3) { + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float* input = static_cast<const float*>(input_buffer); + for (int i = 0; i < 32; ++i) { + // First part (16 frames) contains the reamaining input samples + // from previous iteration (since they were more). + ASSERT_FLOAT_EQ(input[i], 0.01 * (i + 32)); + } + for (int i = 0; i < 16; ++i) { + // next part (8 frames) contains the new input buffer. + ASSERT_FLOAT_EQ(input[i + 32], 0.01 * i); + // last part (8 frames) contains silence. + ASSERT_FLOAT_EQ(input[i + 32 + 16], 0.0); + } + }(); + } + return nframes; +} + +TEST(cubeb, passthrough_resampler_fill_input_left) { + const uint32_t channels = 2; + const uint32_t sample_rate = 44100; + int iteration = 0; + passthrough_resampler<float> resampler = + passthrough_resampler<float>(nullptr, passthrough_resampler_fill_input_left, + &iteration, channels, sample_rate); + + long input_frame_count = 48; // 32 + 16 + const long output_frame_count = 32; + float input[96] = {}; + float output[64] = {}; + for (uint32_t i = 0; i < input_frame_count * channels; ++i) { + input[i] = 0.01 * i; + } + + // 1st iteration, add the extra input. + iteration = 1; + long got = resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used must be equal to output frames. + ASSERT_EQ(input_frame_count, output_frame_count); + + // 2st iteration, use the extra input from previous iteration, + // 16 frames are remaining in the input buffer. + input_frame_count = 32; // we need 16 input frames but we get more; + iteration = 2; + got = resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used must be equal to output frames. + ASSERT_EQ(input_frame_count, output_frame_count); + + // 3rd iteration, use the extra input from previous iteration. + // 16 frames are remaining in the input buffer. + input_frame_count = 16 - 8; // We need 16 more input frames but we only get 8. + iteration = 3; + got = resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used are less than the output frames due to glitch. + ASSERT_EQ(input_frame_count, output_frame_count - 8); +} + +TEST(cubeb, individual_methods) { + const uint32_t channels = 2; + const uint32_t sample_rate = 44100; + const uint32_t frames = 256; + + delay_line<float> dl(10, channels, sample_rate); + uint32_t frames_needed1 = dl.input_needed_for_output(0); + ASSERT_EQ(frames_needed1, 0u); + + cubeb_resampler_speex_one_way<float> one_way(channels, sample_rate, sample_rate, CUBEB_RESAMPLER_QUALITY_DEFAULT); + float buffer[channels * frames] = {0.0}; + // Add all frames in the resampler's internal buffer. + one_way.input(buffer, frames); + // Ask for less than the existing frames, this would create a uint overlflow without the fix. + uint32_t frames_needed2 = one_way.input_needed_for_output(0); + ASSERT_EQ(frames_needed2, 0u); +} + + +#undef NOMINMAX +#undef DUMP_ARRAYS diff --git a/media/libcubeb/test/test_ring_array.cpp b/media/libcubeb/test/test_ring_array.cpp new file mode 100644 index 0000000000..d258d50dbe --- /dev/null +++ b/media/libcubeb/test/test_ring_array.cpp @@ -0,0 +1,73 @@ +#include "gtest/gtest.h" +#ifdef __APPLE__ +#include <string.h> +#include <iostream> +#include <CoreAudio/CoreAudioTypes.h> +#include "cubeb/cubeb.h" +#include "cubeb_ring_array.h" + +TEST(cubeb, ring_array) +{ + ring_array ra; + + ASSERT_EQ(ring_array_init(&ra, 0, 0, 1, 1), CUBEB_ERROR_INVALID_PARAMETER); + ASSERT_EQ(ring_array_init(&ra, 1, 0, 0, 1), CUBEB_ERROR_INVALID_PARAMETER); + + unsigned int capacity = 8; + ring_array_init(&ra, capacity, sizeof(int), 1, 1); + int verify_data[capacity] ;// {1,2,3,4,5,6,7,8}; + AudioBuffer * p_data = NULL; + + for (unsigned int i = 0; i < capacity; ++i) { + verify_data[i] = i; // in case capacity change value + *(int*)ra.buffer_array[i].mData = i; + ASSERT_EQ(ra.buffer_array[i].mDataByteSize, sizeof(int)); + ASSERT_EQ(ra.buffer_array[i].mNumberChannels, 1u); + } + + /* Get store buffers*/ + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_free_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*(int*)p_data->mData, verify_data[i]); + } + /*Now array is full extra store should give NULL*/ + ASSERT_EQ(ring_array_get_free_buffer(&ra), nullptr); + /* Get fetch buffers*/ + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_data_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*(int*)p_data->mData, verify_data[i]); + } + /*Now array is empty extra fetch should give NULL*/ + ASSERT_EQ(ring_array_get_data_buffer(&ra), nullptr); + + p_data = NULL; + /* Repeated store fetch should can go for ever*/ + for (unsigned int i = 0; i < 2*capacity; ++i) { + p_data = ring_array_get_free_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(ring_array_get_data_buffer(&ra), p_data); + } + + p_data = NULL; + /* Verify/modify buffer data*/ + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_free_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*((int*)p_data->mData), verify_data[i]); + (*((int*)p_data->mData))++; // Modify data + } + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_data_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*((int*)p_data->mData), verify_data[i]+1); // Verify modified data + } + + ring_array_destroy(&ra); +} +#else +TEST(cubeb, DISABLED_ring_array) +{ +} +#endif diff --git a/media/libcubeb/test/test_ring_buffer.cpp b/media/libcubeb/test/test_ring_buffer.cpp new file mode 100644 index 0000000000..cfaedd5048 --- /dev/null +++ b/media/libcubeb/test/test_ring_buffer.cpp @@ -0,0 +1,229 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#define NOMINMAX + +#include "gtest/gtest.h" +#include "cubeb_ringbuffer.h" +#include <iostream> +#include <thread> +#include <chrono> + +/* Generate a monotonically increasing sequence of numbers. */ +template<typename T> +class sequence_generator +{ +public: + sequence_generator(size_t channels) + : channels(channels) + { } + void get(T * elements, size_t frames) + { + for (size_t i = 0; i < frames; i++) { + for (size_t c = 0; c < channels; c++) { + elements[i * channels + c] = static_cast<T>(index_); + } + index_++; + } + } + void rewind(size_t frames) + { + index_ -= frames; + } +private: + size_t index_ = 0; + size_t channels = 0; +}; + +/* Checks that a sequence is monotonically increasing. */ +template<typename T> +class sequence_verifier +{ + public: + sequence_verifier(size_t channels) + : channels(channels) + { } + void check(T * elements, size_t frames) + { + for (size_t i = 0; i < frames; i++) { + for (size_t c = 0; c < channels; c++) { + if (elements[i * channels + c] != static_cast<T>(index_)) { + std::cerr << "Element " << i << " is different. Expected " + << static_cast<T>(index_) << ", got " << elements[i] + << ". (channel count: " << channels << ")." << std::endl; + ASSERT_TRUE(false); + } + } + index_++; + } + } + private: + size_t index_ = 0; + size_t channels = 0; +}; + +template<typename T> +void test_ring(lock_free_audio_ring_buffer<T>& buf, int channels, int capacity_frames) +{ + std::unique_ptr<T[]> seq(new T[capacity_frames * channels]); + sequence_generator<T> gen(channels); + sequence_verifier<T> checker(channels); + + int iterations = 1002; + + const int block_size = 128; + + while(iterations--) { + gen.get(seq.get(), block_size); + int rv = buf.enqueue(seq.get(), block_size); + ASSERT_EQ(rv, block_size); + PodZero(seq.get(), block_size); + rv = buf.dequeue(seq.get(), block_size); + ASSERT_EQ(rv, block_size); + checker.check(seq.get(), block_size); + } +} + +template<typename T> +void test_ring_multi(lock_free_audio_ring_buffer<T>& buf, int channels, int capacity_frames) +{ + sequence_verifier<T> checker(channels); + std::unique_ptr<T[]> out_buffer(new T[capacity_frames * channels]); + + const int block_size = 128; + + std::thread t([=, &buf] { + int iterations = 1002; + std::unique_ptr<T[]> in_buffer(new T[capacity_frames * channels]); + sequence_generator<T> gen(channels); + + while(iterations--) { + std::this_thread::yield(); + gen.get(in_buffer.get(), block_size); + int rv = buf.enqueue(in_buffer.get(), block_size); + ASSERT_TRUE(rv <= block_size); + if (rv != block_size) { + gen.rewind(block_size - rv); + } + } + }); + + int remaining = 1002; + + while(remaining--) { + std::this_thread::yield(); + int rv = buf.dequeue(out_buffer.get(), block_size); + ASSERT_TRUE(rv <= block_size); + checker.check(out_buffer.get(), rv); + } + + t.join(); +} + +template<typename T> +void basic_api_test(T& ring) +{ + ASSERT_EQ(ring.capacity(), 128); + + ASSERT_EQ(ring.available_read(), 0); + ASSERT_EQ(ring.available_write(), 128); + + int rv = ring.enqueue_default(63); + + ASSERT_TRUE(rv == 63); + ASSERT_EQ(ring.available_read(), 63); + ASSERT_EQ(ring.available_write(), 65); + + rv = ring.enqueue_default(65); + + ASSERT_EQ(rv, 65); + ASSERT_EQ(ring.available_read(), 128); + ASSERT_EQ(ring.available_write(), 0); + + rv = ring.dequeue(nullptr, 63); + + ASSERT_EQ(ring.available_read(), 65); + ASSERT_EQ(ring.available_write(), 63); + + rv = ring.dequeue(nullptr, 65); + + ASSERT_EQ(ring.available_read(), 0); + ASSERT_EQ(ring.available_write(), 128); +} + +void test_reset_api() { + const size_t ring_buffer_size = 128; + const size_t enqueue_size = ring_buffer_size / 2; + + lock_free_queue<float> ring(ring_buffer_size); + std::thread t([=, &ring] { + std::unique_ptr<float[]> in_buffer(new float[enqueue_size]); + ring.enqueue(in_buffer.get(), enqueue_size); + }); + + t.join(); + + ring.reset_thread_ids(); + + // Enqueue with a different thread. We have reset the thread ID + // in the ring buffer, this should work. + std::thread t2([=, &ring] { + std::unique_ptr<float[]> in_buffer(new float[enqueue_size]); + ring.enqueue(in_buffer.get(), enqueue_size); + }); + + t2.join(); + + ASSERT_TRUE(true); +} + +TEST(cubeb, ring_buffer) +{ + /* Basic API test. */ + const int min_channels = 1; + const int max_channels = 10; + const int min_capacity = 199; + const int max_capacity = 1277; + const int capacity_increment = 27; + + lock_free_queue<float> q1(128); + basic_api_test(q1); + lock_free_queue<short> q2(128); + basic_api_test(q2); + + for (size_t channels = min_channels; channels < max_channels; channels++) { + lock_free_audio_ring_buffer<float> q3(channels, 128); + basic_api_test(q3); + lock_free_audio_ring_buffer<short> q4(channels, 128); + basic_api_test(q4); + } + + /* Single thread testing. */ + /* Test mono to 9.1 */ + for (size_t channels = min_channels; channels < max_channels; channels++) { + /* Use non power-of-two numbers to catch edge-cases. */ + for (size_t capacity_frames = min_capacity; + capacity_frames < max_capacity; capacity_frames+=capacity_increment) { + lock_free_audio_ring_buffer<float> ring(channels, capacity_frames); + test_ring(ring, channels, capacity_frames); + } + } + + /* Multi thread testing */ + for (size_t channels = min_channels; channels < max_channels; channels++) { + /* Use non power-of-two numbers to catch edge-cases. */ + for (size_t capacity_frames = min_capacity; + capacity_frames < max_capacity; capacity_frames+=capacity_increment) { + lock_free_audio_ring_buffer<short> ring(channels, capacity_frames); + test_ring_multi(ring, channels, capacity_frames); + } + } + + test_reset_api(); +} + +#undef NOMINMAX diff --git a/media/libcubeb/test/test_sanity.cpp b/media/libcubeb/test/test_sanity.cpp new file mode 100644 index 0000000000..d681c502bb --- /dev/null +++ b/media/libcubeb/test/test_sanity.cpp @@ -0,0 +1,698 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <atomic> +#include <stdio.h> +#include <string.h> +#include <math.h> + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + +#define STREAM_RATE 44100 +#define STREAM_LATENCY 100 * STREAM_RATE / 1000 +#define STREAM_CHANNELS 1 +#define STREAM_LAYOUT CUBEB_LAYOUT_MONO +#define STREAM_FORMAT CUBEB_SAMPLE_S16LE + +int is_windows_7() +{ +#ifdef __MINGW32__ + fprintf(stderr, "Warning: this test was built with MinGW.\n" + "MinGW does not contain necessary version checking infrastructure. Claiming to be Windows 7, even if we're not.\n"); + return 1; +#endif +#if (defined(_WIN32) || defined(__WIN32__)) && ( !defined(__MINGW32__)) + OSVERSIONINFOEX osvi; + DWORDLONG condition_mask = 0; + + ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); + osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + + // NT 6.1 is Windows 7 + osvi.dwMajorVersion = 6; + osvi.dwMinorVersion = 1; + + VER_SET_CONDITION(condition_mask, VER_MAJORVERSION, VER_EQUAL); + VER_SET_CONDITION(condition_mask, VER_MINORVERSION, VER_GREATER_EQUAL); + + return VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION, condition_mask); +#else + return 0; +#endif +} + +static int dummy; +static std::atomic<uint64_t> total_frames_written; +static int delay_callback; + +static long +test_data_callback(cubeb_stream * stm, void * user_ptr, const void * /*inputbuffer*/, void * outputbuffer, long nframes) +{ + EXPECT_TRUE(stm && user_ptr == &dummy && outputbuffer && nframes > 0); + assert(outputbuffer); + memset(outputbuffer, 0, nframes * sizeof(short)); + + total_frames_written += nframes; + if (delay_callback) { + delay(10); + } + return nframes; +} + +void +test_state_callback(cubeb_stream * /*stm*/, void * /*user_ptr*/, cubeb_state /*state*/) +{ +} + +TEST(cubeb, init_destroy_context) +{ + int r; + cubeb * ctx; + char const* backend_id; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + backend_id = cubeb_get_backend_id(ctx); + ASSERT_TRUE(backend_id); + + fprintf(stderr, "Backend: %s\n", backend_id); + + cubeb_destroy(ctx); +} + +TEST(cubeb, init_destroy_multiple_contexts) +{ + size_t i; + int r; + cubeb * ctx[4]; + int order[4] = {2, 0, 3, 1}; + ASSERT_EQ(ARRAY_LENGTH(ctx), ARRAY_LENGTH(order)); + + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + r = common_init(&ctx[i], NULL); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx[i], nullptr); + } + + /* destroy in a different order */ + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + cubeb_destroy(ctx[order[i]]); + } +} + +TEST(cubeb, context_variables) +{ + int r; + cubeb * ctx; + uint32_t value; + cubeb_stream_params params; + + r = common_init(&ctx, "test_context_variables"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.channels = STREAM_CHANNELS; + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, ¶ms, &value); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_TRUE(value > 0); + } + + r = cubeb_get_preferred_sample_rate(ctx, &value); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_TRUE(value > 0); + } + + cubeb_destroy(ctx); +} + +TEST(cubeb, init_destroy_stream) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +TEST(cubeb, init_destroy_multiple_streams) +{ + size_t i; + int r; + cubeb * ctx; + cubeb_stream * stream[8]; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_init(ctx, &stream[i], "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream[i], nullptr); + } + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + cubeb_stream_destroy(stream[i]); + } + + cubeb_destroy(ctx); +} + +TEST(cubeb, configure_stream) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = 2; + params.layout = CUBEB_LAYOUT_STEREO; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + r = cubeb_stream_set_volume(stream, 1.0f); + ASSERT_TRUE(r == 0 || r == CUBEB_ERROR_NOT_SUPPORTED); + + r = cubeb_stream_set_name(stream, "test 2"); + ASSERT_TRUE(r == 0 || r == CUBEB_ERROR_NOT_SUPPORTED); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +TEST(cubeb, configure_stream_undefined_layout) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = 2; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + delay(100); + + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +static void +test_init_start_stop_destroy_multiple_streams(int early, int delay_ms) +{ + size_t i; + int r; + cubeb * ctx; + cubeb_stream * stream[8]; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_init(ctx, &stream[i], "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream[i], nullptr); + if (early) { + r = cubeb_stream_start(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + } + + if (!early) { + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_start(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + } + + if (delay_ms) { + delay(delay_ms); + } + + if (!early) { + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_stop(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + } + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + if (early) { + r = cubeb_stream_stop(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + cubeb_stream_destroy(stream[i]); + } + + cubeb_destroy(ctx); +} + +TEST(cubeb, init_start_stop_destroy_multiple_streams) +{ + /* Sometimes, when using WASAPI on windows 7 (vista and 8 are okay), and + * calling Activate a lot on an AudioClient, 0x800700b7 is returned. This is + * the HRESULT value for "Cannot create a file when that file already exists", + * and is not documented as a possible return value for this call. Hence, we + * try to limit the number of streams we create in this test. */ + if (!is_windows_7()) { + delay_callback = 0; + test_init_start_stop_destroy_multiple_streams(0, 0); + test_init_start_stop_destroy_multiple_streams(1, 0); + test_init_start_stop_destroy_multiple_streams(0, 150); + test_init_start_stop_destroy_multiple_streams(1, 150); + delay_callback = 1; + test_init_start_stop_destroy_multiple_streams(0, 0); + test_init_start_stop_destroy_multiple_streams(1, 0); + test_init_start_stop_destroy_multiple_streams(0, 150); + test_init_start_stop_destroy_multiple_streams(1, 150); + } +} + +TEST(cubeb, init_destroy_multiple_contexts_and_streams) +{ + size_t i, j; + int r; + cubeb * ctx[2]; + cubeb_stream * stream[8]; + cubeb_stream_params params; + size_t streams_per_ctx = ARRAY_LENGTH(stream) / ARRAY_LENGTH(ctx); + ASSERT_EQ(ARRAY_LENGTH(ctx) * streams_per_ctx, ARRAY_LENGTH(stream)); + + /* Sometimes, when using WASAPI on windows 7 (vista and 8 are okay), and + * calling Activate a lot on an AudioClient, 0x800700b7 is returned. This is + * the HRESULT value for "Cannot create a file when that file already exists", + * and is not documented as a possible return value for this call. Hence, we + * try to limit the number of streams we create in this test. */ + if (is_windows_7()) + return; + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + r = common_init(&ctx[i], "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx[i], nullptr); + + for (j = 0; j < streams_per_ctx; ++j) { + r = cubeb_stream_init(ctx[i], &stream[i * streams_per_ctx + j], "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream[i * streams_per_ctx + j], nullptr); + } + } + + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + for (j = 0; j < streams_per_ctx; ++j) { + cubeb_stream_destroy(stream[i * streams_per_ctx + j]); + } + cubeb_destroy(ctx[i]); + } +} + +TEST(cubeb, basic_stream_operations) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + uint64_t position; + uint32_t latency; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + /* position and latency before stream has started */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(position, 0u); + + r = cubeb_stream_get_latency(stream, &latency); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* position and latency after while stream running */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_get_latency(stream, &latency); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* position and latency after stream has stopped */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_get_latency(stream, &latency); + ASSERT_EQ(r, CUBEB_OK); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +TEST(cubeb, stream_position) +{ + size_t i; + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + uint64_t position, last_position; + + total_frames_written = 0; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + /* stream position should not advance before starting playback */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(position, 0u); + + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(position, 0u); + + /* stream position should advance during playback */ + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* XXX let start happen */ + delay(500); + + /* stream should have prefilled */ + ASSERT_TRUE(total_frames_written.load() > 0); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + last_position = position; + + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_GE(position, last_position); + last_position = position; + + /* stream position should not exceed total frames written */ + for (i = 0; i < 5; ++i) { + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_GE(position, last_position); + ASSERT_LE(position, total_frames_written.load()); + last_position = position; + delay(500); + } + + /* test that the position is valid even when starting and + * stopping the stream. */ + for (i = 0; i < 5; ++i) { + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_TRUE(last_position < position); + last_position = position; + delay(500); + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + delay(500); + } + + ASSERT_NE(last_position, 0u); + + /* stream position should not advance after stopping playback */ + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* XXX allow stream to settle */ + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + last_position = position; + + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(position, last_position); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +static std::atomic<int> do_drain; +static std::atomic<int> got_drain; + +static long +test_drain_data_callback(cubeb_stream * stm, void * user_ptr, const void * /*inputbuffer*/, void * outputbuffer, long nframes) +{ + EXPECT_TRUE(stm && user_ptr == &dummy && outputbuffer && nframes > 0); + assert(outputbuffer); + if (do_drain == 1) { + do_drain = 2; + return 0; + } + /* once drain has started, callback must never be called again */ + EXPECT_TRUE(do_drain != 2); + memset(outputbuffer, 0, nframes * sizeof(short)); + total_frames_written += nframes; + return nframes; +} + +void +test_drain_state_callback(cubeb_stream * /*stm*/, void * /*user_ptr*/, cubeb_state state) +{ + if (state == CUBEB_STATE_DRAINED) { + ASSERT_TRUE(!got_drain); + got_drain = 1; + } +} + +TEST(cubeb, drain) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + uint64_t position; + + delay_callback = 0; + total_frames_written = 0; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_drain_data_callback, test_drain_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + delay(5000); + + do_drain = 1; + + for (;;) { + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + if (got_drain) { + break; + } else { + ASSERT_LE(position, total_frames_written.load()); + } + delay(500); + } + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_TRUE(got_drain); + + // Really, we should be able to rely on position reaching our final written frame, but + // for now let's make sure it doesn't continue beyond that point. + //ASSERT_LE(position, total_frames_written.load()); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); + + got_drain = 0; + do_drain = 0; +} + +TEST(cubeb, DISABLED_eos_during_prefill) +{ + // This test needs to be implemented. +} + +TEST(cubeb, DISABLED_stream_destroy_pending_drain) +{ + // This test needs to be implemented. +} + +TEST(cubeb, stable_devid) +{ + /* Test that the devid field of cubeb_device_info is stable + * (ie. compares equal) over two invocations of + * cubeb_enumerate_devices(). */ + + int r; + cubeb * ctx; + cubeb_device_collection first; + cubeb_device_collection second; + cubeb_device_type all_devices = + (cubeb_device_type) (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT); + size_t n; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + r = cubeb_enumerate_devices(ctx, all_devices, &first); + if (r == CUBEB_ERROR_NOT_SUPPORTED) + return; + + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_enumerate_devices(ctx, all_devices, &second); + ASSERT_EQ(r, CUBEB_OK); + + ASSERT_EQ(first.count, second.count); + for (n = 0; n < first.count; n++) { + ASSERT_EQ(first.device[n].devid, second.device[n].devid); + } + + r = cubeb_device_collection_destroy(ctx, &first); + ASSERT_EQ(r, CUBEB_OK); + r = cubeb_device_collection_destroy(ctx, &second); + ASSERT_EQ(r, CUBEB_OK); + cubeb_destroy(ctx); +} + +#undef STREAM_RATE +#undef STREAM_LATENCY +#undef STREAM_CHANNELS +#undef STREAM_LAYOUT +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_tone.cpp b/media/libcubeb/test/test_tone.cpp new file mode 100644 index 0000000000..9d0e1afd32 --- /dev/null +++ b/media/libcubeb/test/test_tone.cpp @@ -0,0 +1,124 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Plays a simple tone. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <memory> +#include <limits.h> +#include "cubeb/cubeb.h" +#include <atomic> + +//#define ENABLE_NORMAL_LOG +//#define ENABLE_VERBOSE_LOG +#include "common.h" + + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_S16LE + +/* store the phase of the generated waveform */ +struct cb_user_data { + std::atomic<long> position; +}; + +long data_cb_tone(cubeb_stream *stream, void *user, const void* /*inputbuffer*/, void *outputbuffer, long nframes) +{ + struct cb_user_data *u = (struct cb_user_data *)user; + short *b = (short *)outputbuffer; + float t1, t2; + int i; + + if (stream == NULL || u == NULL) + return CUBEB_ERROR; + + /* generate our test tone on the fly */ + for (i = 0; i < nframes; i++) { + /* North American dial tone */ + t1 = sin(2*M_PI*(i + u->position)*350/SAMPLE_FREQUENCY); + t2 = sin(2*M_PI*(i + u->position)*440/SAMPLE_FREQUENCY); + b[i] = (SHRT_MAX / 2) * t1; + b[i] += (SHRT_MAX / 2) * t2; + /* European dial tone */ + /* + t1 = sin(2*M_PI*(i + u->position)*425/SAMPLE_FREQUENCY); + b[i] = SHRT_MAX * t1; + */ + } + /* remember our phase to avoid clicking on buffer transitions */ + /* we'll still click if position overflows */ + u->position += nframes; + + return nframes; +} + +void state_cb_tone(cubeb_stream *stream, void *user, cubeb_state state) +{ + struct cb_user_data *u = (struct cb_user_data *)user; + + if (stream == NULL || u == 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; +} + +TEST(cubeb, tone) +{ + cubeb *ctx; + cubeb_stream *stream; + cubeb_stream_params params; + int r; + + r = common_init(&ctx, "Cubeb tone example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + params.format = STREAM_FORMAT; + params.rate = SAMPLE_FREQUENCY; + params.channels = 1; + params.layout = CUBEB_LAYOUT_MONO; + params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr<cb_user_data> user_data(new cb_user_data()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + user_data->position = 0; + + r = cubeb_stream_init(ctx, &stream, "Cubeb tone (mono)", NULL, NULL, NULL, ¶ms, + 4096, data_cb_tone, state_cb_tone, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(5000); + cubeb_stream_stop(stream); + + ASSERT_TRUE(user_data->position.load()); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_triple_buffer.cpp b/media/libcubeb/test/test_triple_buffer.cpp new file mode 100644 index 0000000000..a6e0049b79 --- /dev/null +++ b/media/libcubeb/test/test_triple_buffer.cpp @@ -0,0 +1,67 @@ +/* + * Copyright © 2022 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* cubeb_triple_buffer test */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include "cubeb_triple_buffer.h" +#include <atomic> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> +#include <thread> + +#include "common.h" + +TEST(cubeb, triple_buffer) +{ + struct AB { + uint64_t a; + uint64_t b; + }; + triple_buffer<AB> buffer; + + std::atomic<bool> finished = {false}; + + ASSERT_TRUE(!buffer.updated()); + + auto t = std::thread([&finished, &buffer] { + AB ab; + ab.a = 0; + ab.b = UINT64_MAX; + uint64_t counter = 0; + do { + buffer.write(ab); + ab.a++; + ab.b--; + } while (counter++ < 1e6 && ab.a <= UINT64_MAX && ab.b != 0); + finished.store(true); + }); + + AB ab; + AB old_ab; + old_ab.a = 0; + old_ab.b = UINT64_MAX; + + // Wait to have at least one value produced. + while (!buffer.updated()) { + } + + // Check that the values are increasing (resp. descreasing) monotonically. + while (!finished) { + ab = buffer.read(); + ASSERT_GE(ab.a, old_ab.a); + ASSERT_LE(ab.b, old_ab.b); + old_ab = ab; + } + + t.join(); +} diff --git a/media/libcubeb/test/test_utils.cpp b/media/libcubeb/test/test_utils.cpp new file mode 100644 index 0000000000..cbdb960984 --- /dev/null +++ b/media/libcubeb/test/test_utils.cpp @@ -0,0 +1,72 @@ +#include "gtest/gtest.h" +#include "cubeb_utils.h" + +TEST(cubeb, auto_array) +{ + auto_array<uint32_t> array; + auto_array<uint32_t> array2(10); + uint32_t a[10]; + + ASSERT_EQ(array2.length(), 0u); + ASSERT_EQ(array2.capacity(), 10u); + + + for (uint32_t i = 0; i < 10; i++) { + a[i] = i; + } + + ASSERT_EQ(array.capacity(), 0u); + ASSERT_EQ(array.length(), 0u); + + array.push(a, 10); + + ASSERT_TRUE(!array.reserve(9)); + + for (uint32_t i = 0; i < 10; i++) { + ASSERT_EQ(array.data()[i], i); + } + + ASSERT_EQ(array.capacity(), 10u); + ASSERT_EQ(array.length(), 10u); + + uint32_t b[10]; + + array.pop(b, 5); + + ASSERT_EQ(array.capacity(), 10u); + ASSERT_EQ(array.length(), 5u); + for (uint32_t i = 0; i < 5; i++) { + ASSERT_EQ(b[i], i); + ASSERT_EQ(array.data()[i], 5 + i); + } + uint32_t* bb = b + 5; + array.pop(bb, 5); + + ASSERT_EQ(array.capacity(), 10u); + ASSERT_EQ(array.length(), 0u); + for (uint32_t i = 0; i < 5; i++) { + ASSERT_EQ(bb[i], 5 + i); + } + + ASSERT_TRUE(!array.pop(nullptr, 1)); + + array.push(a, 10); + array.push(a, 10); + + for (uint32_t j = 0; j < 2; j++) { + for (uint32_t i = 0; i < 10; i++) { + ASSERT_EQ(array.data()[10 * j + i], i); + } + } + ASSERT_EQ(array.length(), 20u); + ASSERT_EQ(array.capacity(), 20u); + array.pop(nullptr, 5); + + for (uint32_t i = 0; i < 5; i++) { + ASSERT_EQ(array.data()[i], 5 + i); + } + + ASSERT_EQ(array.length(), 15u); + ASSERT_EQ(array.capacity(), 20u); +} + |