From 77b1b7585c0d38fad6a2188046c0c6896c277eea Mon Sep 17 00:00:00 2001 From: Alex Chronopoulos Date: Fri, 17 May 2019 16:38:04 +0000 Subject: [PATCH] Bug 1545079 - Enhance MockCubeb class to simulate a cubeb stream. r=padenot On MochCubeb add a fake audio thread and the corresponding methods for stream_{init,start,stop,destroy}. Differential Revision: https://phabricator.services.mozilla.com/D30888 --HG-- extra : moz-landing-system : lando --- dom/media/gtest/MockCubeb.h | 203 ++++++++++++++++++++++++++++++++---- 1 file changed, 181 insertions(+), 22 deletions(-) diff --git a/dom/media/gtest/MockCubeb.h b/dom/media/gtest/MockCubeb.h index 04179b79d724..19c49ac7da0e 100644 --- a/dom/media/gtest/MockCubeb.h +++ b/dom/media/gtest/MockCubeb.h @@ -3,6 +3,15 @@ #include "AudioDeviceInfo.h" +#include +#include +#include + +using namespace std::chrono_literals; + +const long NUM_OF_FRAMES = 512; +const uint32_t NUM_OF_CHANNELS = 2; + struct cubeb_ops { int (*init)(cubeb** context, char const* context_name); char const* (*get_backend_id)(cubeb* context); @@ -54,24 +63,45 @@ static int cubeb_mock_register_device_collection_changed( cubeb* context, cubeb_device_type devtype, cubeb_device_collection_changed_callback callback, void* user_ptr); +static int cubeb_mock_stream_init( + cubeb* context, cubeb_stream** stream, char const* stream_name, + cubeb_devid input_device, cubeb_stream_params* input_stream_params, + cubeb_devid output_device, cubeb_stream_params* output_stream_params, + unsigned int latency, cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void* user_ptr); + +static int cubeb_mock_stream_start(cubeb_stream* stream); + +static int cubeb_mock_stream_stop(cubeb_stream* stream); + +static void cubeb_mock_stream_destroy(cubeb_stream* stream); + +static char const* cubeb_mock_get_backend_id(cubeb* context); + +static int cubeb_mock_stream_set_volume(cubeb_stream* stream, float volume); + +static int cubeb_mock_get_min_latency(cubeb* context, + cubeb_stream_params params, + uint32_t* latency_ms); + // Mock cubeb impl, only supports device enumeration for now. cubeb_ops const mock_ops = { /*.init =*/NULL, - /*.get_backend_id =*/NULL, + /*.get_backend_id =*/cubeb_mock_get_backend_id, /*.get_max_channel_count =*/NULL, - /*.get_min_latency =*/NULL, + /*.get_min_latency =*/cubeb_mock_get_min_latency, /*.get_preferred_sample_rate =*/NULL, /*.enumerate_devices =*/cubeb_mock_enumerate_devices, /*.device_collection_destroy =*/cubeb_mock_device_collection_destroy, /*.destroy =*/cubeb_mock_destroy, - /*.stream_init =*/NULL, - /*.stream_destroy =*/NULL, - /*.stream_start =*/NULL, - /*.stream_stop =*/NULL, + /*.stream_init =*/cubeb_mock_stream_init, + /*.stream_destroy =*/cubeb_mock_stream_destroy, + /*.stream_start =*/cubeb_mock_stream_start, + /*.stream_stop =*/cubeb_mock_stream_stop, /*.stream_reset_default_device =*/NULL, /*.stream_get_position =*/NULL, /*.stream_get_latency =*/NULL, - /*.stream_set_volume =*/NULL, + /*.stream_set_volume =*/cubeb_mock_stream_set_volume, /*.stream_set_panning =*/NULL, /*.stream_get_current_device =*/NULL, /*.stream_device_destroy =*/NULL, @@ -85,13 +115,11 @@ cubeb_ops const mock_ops = { // should do, depending on what is being tested. class MockCubeb { public: - MockCubeb() - : ops(&mock_ops), - mInputDeviceCollectionChangeCallback(nullptr), - mOutputDeviceCollectionChangeCallback(nullptr), - mInputDeviceCollectionChangeUserPtr(nullptr), - mOutputDeviceCollectionChangeUserPtr(nullptr), - mSupportsDeviceCollectionChangedCallback(true) {} + MockCubeb() : ops(&mock_ops) {} + ~MockCubeb() { + assert(!mFakeAudioThread); + assert(!mMockStream); + } // Cubeb backend implementation // This allows passing this class as a cubeb* instance. cubeb* AsCubebContext() { return reinterpret_cast(this); } @@ -241,33 +269,109 @@ class MockCubeb { mSupportsDeviceCollectionChangedCallback = aSupports; } + // Represents the fake cubeb_stream. The context instance is needed to + // provide access on cubeb_ops struct. + struct MockCubebStream { + cubeb* context = nullptr; + }; + + // Simulates the audio thread. The thread is created at StreamStart and + // destroyed at StreamStop. At next StreamStart a new thread is created. + static void ThreadFunction_s(MockCubeb* that) { that->ThreadFunction(); } + + void ThreadFunction() { + while (!mStreamStop) { + cubeb_stream* stream = reinterpret_cast(mMockStream.get()); + long outframes = mDataCallback(stream, mUserPtr, nullptr, mOutputBuffer, + NUM_OF_FRAMES); + if (outframes < NUM_OF_FRAMES) { + mStateCallback(stream, mUserPtr, CUBEB_STATE_DRAINED); + break; + } + std::this_thread::sleep_for( + std::chrono::milliseconds(NUM_OF_FRAMES * 1000 / mSampleRate)); + } + } + + int StreamInit(cubeb* aContext, cubeb_stream** aStream, + cubeb_stream_params* aInputStreamParams, + cubeb_stream_params* aOutputStreamParams, + cubeb_data_callback aDataCallback, + cubeb_state_callback aStateCallback, void* aUserPtr) { + assert(!mFakeAudioThread); + mMockStream.reset(new MockCubebStream); + mMockStream->context = aContext; + *aStream = reinterpret_cast(mMockStream.get()); + mDataCallback = aDataCallback; + mStateCallback = aStateCallback; + mUserPtr = aUserPtr; + mSampleRate = aInputStreamParams ? aInputStreamParams->rate + : aOutputStreamParams->rate; + return CUBEB_OK; + } + + int StreamStart(cubeb_stream* aStream) { + assert(!mFakeAudioThread); + mStreamStop = false; + mFakeAudioThread.reset(new std::thread(ThreadFunction_s, this)); + assert(mFakeAudioThread); + mStateCallback(aStream, mUserPtr, CUBEB_STATE_STARTED); + return CUBEB_OK; + } + + int StreamStop(cubeb_stream* aStream) { + assert(mFakeAudioThread); + mStreamStop = true; + mFakeAudioThread->join(); + mFakeAudioThread.reset(); + mStateCallback(aStream, mUserPtr, CUBEB_STATE_STOPPED); + return CUBEB_OK; + } + + void StreamDestroy(cubeb_stream* aStream) { mMockStream.reset(); } + private: // This needs to have the exact same memory layout as a real cubeb backend. // It's very important for this `ops` member to be the very first member of // the class, and to not have any virtual members (to avoid having a vtable). const cubeb_ops* ops; // The callback to call when the device list has been changed. - cubeb_device_collection_changed_callback mInputDeviceCollectionChangeCallback; cubeb_device_collection_changed_callback - mOutputDeviceCollectionChangeCallback; + mInputDeviceCollectionChangeCallback = nullptr; + cubeb_device_collection_changed_callback + mOutputDeviceCollectionChangeCallback = nullptr; + cubeb_data_callback mDataCallback = nullptr; + cubeb_state_callback mStateCallback = nullptr; // The pointer to pass in the callback. - void* mInputDeviceCollectionChangeUserPtr; - void* mOutputDeviceCollectionChangeUserPtr; + void* mInputDeviceCollectionChangeUserPtr = nullptr; + void* mOutputDeviceCollectionChangeUserPtr = nullptr; + void* mUserPtr = nullptr; // Whether or not this backend supports device collection change notification // via a system callback. If not, Gecko is expected to re-query the list every // time. - bool mSupportsDeviceCollectionChangedCallback; + bool mSupportsDeviceCollectionChangedCallback = true; // Our input and output devices. nsTArray mInputDevices; nsTArray mOutputDevices; + + // Thread that simulates the audio thread. + std::unique_ptr mFakeAudioThread; + // Signal to the audio thread that stream is stopped. + std::atomic_bool mStreamStop{true}; + // The fake stream instance. + std::unique_ptr mMockStream; + // The stream sample rate + uint32_t mSampleRate = 0; + // The audio buffer used on data callback. + float mOutputBuffer[NUM_OF_CHANNELS * NUM_OF_FRAMES]; }; void cubeb_mock_destroy(cubeb* context) { delete reinterpret_cast(context); } -static int cubeb_mock_enumerate_devices(cubeb* context, cubeb_device_type type, - cubeb_device_collection* out) { +int cubeb_mock_enumerate_devices(cubeb* context, cubeb_device_type type, + cubeb_device_collection* out) { MockCubeb* mock = reinterpret_cast(context); return mock->EnumerateDevices(type, out); } @@ -286,6 +390,61 @@ int cubeb_mock_register_device_collection_changed( user_ptr); } +int cubeb_mock_stream_init( + cubeb* context, cubeb_stream** stream, char const* stream_name, + cubeb_devid input_device, cubeb_stream_params* input_stream_params, + cubeb_devid output_device, cubeb_stream_params* output_stream_params, + unsigned int latency, cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void* user_ptr) { + MockCubeb* mock = reinterpret_cast(context); + return mock->StreamInit(context, stream, input_stream_params, + output_stream_params, data_callback, state_callback, + user_ptr); +} + +int cubeb_mock_stream_start(cubeb_stream* stream) { + MockCubeb::MockCubebStream* mockStream = + reinterpret_cast(stream); + MockCubeb* mock = reinterpret_cast(mockStream->context); + return mock->StreamStart(stream); +} + +int cubeb_mock_stream_stop(cubeb_stream* stream) { + MockCubeb::MockCubebStream* mockStream = + reinterpret_cast(stream); + MockCubeb* mock = reinterpret_cast(mockStream->context); + return mock->StreamStop(stream); +} + +void cubeb_mock_stream_destroy(cubeb_stream* stream) { + MockCubeb::MockCubebStream* mockStream = + reinterpret_cast(stream); + MockCubeb* mock = reinterpret_cast(mockStream->context); + return mock->StreamDestroy(stream); +} + +static char const* cubeb_mock_get_backend_id(cubeb* context) { +#if defined(XP_LINUX) + return "pulse"; +#elif defined(XP_MACOSX) + return "audiounit"; +#elif defined(XP_WIN) + return "wasapi"; +#elif defined(ANDROID) + return "opensl"; +#endif +} + +static int cubeb_mock_stream_set_volume(cubeb_stream* stream, float volume) { + return CUBEB_OK; +} + +int cubeb_mock_get_min_latency(cubeb* context, cubeb_stream_params params, + uint32_t* latency_ms) { + *latency_ms = NUM_OF_FRAMES; + return CUBEB_OK; +} + void PrintDevice(cubeb_device_info aInfo) { printf( "id: %zu\n" @@ -412,4 +571,4 @@ void AddDevices(MockCubeb* mock, uint32_t device_count, } } -#endif // MOCKCUBEB_H_ +#endif // MOCKCUBEB_H_