diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/libwebrtc/sdk/objc/unittests/RTCAudioDeviceModule_xctest.mm | 593 |
1 files changed, 593 insertions, 0 deletions
diff --git a/third_party/libwebrtc/sdk/objc/unittests/RTCAudioDeviceModule_xctest.mm b/third_party/libwebrtc/sdk/objc/unittests/RTCAudioDeviceModule_xctest.mm new file mode 100644 index 0000000000..f8ce844652 --- /dev/null +++ b/third_party/libwebrtc/sdk/objc/unittests/RTCAudioDeviceModule_xctest.mm @@ -0,0 +1,593 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import <XCTest/XCTest.h> + +#if defined(WEBRTC_IOS) +#import "sdk/objc/native/api/audio_device_module.h" +#endif + +#include "api/scoped_refptr.h" + +typedef int32_t(^NeedMorePlayDataBlock)(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void* audioSamples, + size_t& nSamplesOut, + int64_t* elapsed_time_ms, + int64_t* ntp_time_ms); + +typedef int32_t(^RecordedDataIsAvailableBlock)(const void* audioSamples, + const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + const uint32_t totalDelayMS, + const int32_t clockDrift, + const uint32_t currentMicLevel, + const bool keyPressed, + uint32_t& newMicLevel); + + +// This class implements the AudioTransport API and forwards all methods to the appropriate blocks. +class MockAudioTransport : public webrtc::AudioTransport { +public: + MockAudioTransport() {} + ~MockAudioTransport() override {} + + void expectNeedMorePlayData(NeedMorePlayDataBlock block) { + needMorePlayDataBlock = block; + } + + void expectRecordedDataIsAvailable(RecordedDataIsAvailableBlock block) { + recordedDataIsAvailableBlock = block; + } + + int32_t NeedMorePlayData(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void* audioSamples, + size_t& nSamplesOut, + int64_t* elapsed_time_ms, + int64_t* ntp_time_ms) override { + return needMorePlayDataBlock(nSamples, + nBytesPerSample, + nChannels, + samplesPerSec, + audioSamples, + nSamplesOut, + elapsed_time_ms, + ntp_time_ms); + } + + int32_t RecordedDataIsAvailable(const void* audioSamples, + const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + const uint32_t totalDelayMS, + const int32_t clockDrift, + const uint32_t currentMicLevel, + const bool keyPressed, + uint32_t& newMicLevel) override { + return recordedDataIsAvailableBlock(audioSamples, + nSamples, + nBytesPerSample, + nChannels, + samplesPerSec, + totalDelayMS, + clockDrift, + currentMicLevel, + keyPressed, + newMicLevel); + } + + void PullRenderData(int bits_per_sample, + int sample_rate, + size_t number_of_channels, + size_t number_of_frames, + void* audio_data, + int64_t* elapsed_time_ms, + int64_t* ntp_time_ms) override {} + + private: + NeedMorePlayDataBlock needMorePlayDataBlock; + RecordedDataIsAvailableBlock recordedDataIsAvailableBlock; +}; + +// Number of callbacks (input or output) the tests waits for before we set +// an event indicating that the test was OK. +static const NSUInteger kNumCallbacks = 10; +// Max amount of time we wait for an event to be set while counting callbacks. +static const NSTimeInterval kTestTimeOutInSec = 20.0; +// Number of bits per PCM audio sample. +static const NSUInteger kBitsPerSample = 16; +// Number of bytes per PCM audio sample. +static const NSUInteger kBytesPerSample = kBitsPerSample / 8; +// Average number of audio callbacks per second assuming 10ms packet size. +static const NSUInteger kNumCallbacksPerSecond = 100; +// Play out a test file during this time (unit is in seconds). +static const NSUInteger kFilePlayTimeInSec = 15; +// Run the full-duplex test during this time (unit is in seconds). +// Note that first `kNumIgnoreFirstCallbacks` are ignored. +static const NSUInteger kFullDuplexTimeInSec = 10; +// Wait for the callback sequence to stabilize by ignoring this amount of the +// initial callbacks (avoids initial FIFO access). +// Only used in the RunPlayoutAndRecordingInFullDuplex test. +static const NSUInteger kNumIgnoreFirstCallbacks = 50; + +@interface RTCAudioDeviceModuleTests : XCTestCase { + rtc::scoped_refptr<webrtc::AudioDeviceModule> audioDeviceModule; + MockAudioTransport mock; +} + +@property(nonatomic, assign) webrtc::AudioParameters playoutParameters; +@property(nonatomic, assign) webrtc::AudioParameters recordParameters; + +@end + +@implementation RTCAudioDeviceModuleTests + +@synthesize playoutParameters; +@synthesize recordParameters; + +- (void)setUp { + [super setUp]; + audioDeviceModule = webrtc::CreateAudioDeviceModule(); + XCTAssertEqual(0, audioDeviceModule->Init()); + XCTAssertEqual(0, audioDeviceModule->GetPlayoutAudioParameters(&playoutParameters)); + XCTAssertEqual(0, audioDeviceModule->GetRecordAudioParameters(&recordParameters)); +} + +- (void)tearDown { + XCTAssertEqual(0, audioDeviceModule->Terminate()); + audioDeviceModule = nullptr; + [super tearDown]; +} + +- (void)startPlayout { + XCTAssertFalse(audioDeviceModule->Playing()); + XCTAssertEqual(0, audioDeviceModule->InitPlayout()); + XCTAssertTrue(audioDeviceModule->PlayoutIsInitialized()); + XCTAssertEqual(0, audioDeviceModule->StartPlayout()); + XCTAssertTrue(audioDeviceModule->Playing()); +} + +- (void)stopPlayout { + XCTAssertEqual(0, audioDeviceModule->StopPlayout()); + XCTAssertFalse(audioDeviceModule->Playing()); +} + +- (void)startRecording{ + XCTAssertFalse(audioDeviceModule->Recording()); + XCTAssertEqual(0, audioDeviceModule->InitRecording()); + XCTAssertTrue(audioDeviceModule->RecordingIsInitialized()); + XCTAssertEqual(0, audioDeviceModule->StartRecording()); + XCTAssertTrue(audioDeviceModule->Recording()); +} + +- (void)stopRecording{ + XCTAssertEqual(0, audioDeviceModule->StopRecording()); + XCTAssertFalse(audioDeviceModule->Recording()); +} + +- (NSURL*)fileURLForSampleRate:(int)sampleRate { + XCTAssertTrue(sampleRate == 48000 || sampleRate == 44100 || sampleRate == 16000); + NSString *filename = [NSString stringWithFormat:@"audio_short%d", sampleRate / 1000]; + NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:@"pcm"]; + XCTAssertNotNil(url); + + return url; +} + +#pragma mark - Tests + +- (void)testConstructDestruct { + // Using the test fixture to create and destruct the audio device module. +} + +- (void)testInitTerminate { + // Initialization is part of the test fixture. + XCTAssertTrue(audioDeviceModule->Initialized()); + XCTAssertEqual(0, audioDeviceModule->Terminate()); + XCTAssertFalse(audioDeviceModule->Initialized()); +} + +// Tests that playout can be initiated, started and stopped. No audio callback +// is registered in this test. +- (void)testStartStopPlayout { + [self startPlayout]; + [self stopPlayout]; + [self startPlayout]; + [self stopPlayout]; +} + +// Tests that recording can be initiated, started and stopped. No audio callback +// is registered in this test. +- (void)testStartStopRecording { + [self startRecording]; + [self stopRecording]; + [self startRecording]; + [self stopRecording]; +} +// Verify that calling StopPlayout() will leave us in an uninitialized state +// which will require a new call to InitPlayout(). This test does not call +// StartPlayout() while being uninitialized since doing so will hit a +// RTC_DCHECK. +- (void)testStopPlayoutRequiresInitToRestart { + XCTAssertEqual(0, audioDeviceModule->InitPlayout()); + XCTAssertEqual(0, audioDeviceModule->StartPlayout()); + XCTAssertEqual(0, audioDeviceModule->StopPlayout()); + XCTAssertFalse(audioDeviceModule->PlayoutIsInitialized()); +} + +// Verify that we can create two ADMs and start playing on the second ADM. +// Only the first active instance shall activate an audio session and the +// last active instance shall deactivate the audio session. The test does not +// explicitly verify correct audio session calls but instead focuses on +// ensuring that audio starts for both ADMs. +- (void)testStartPlayoutOnTwoInstances { + // Create and initialize a second/extra ADM instance. The default ADM is + // created by the test harness. + rtc::scoped_refptr<webrtc::AudioDeviceModule> secondAudioDeviceModule = + webrtc::CreateAudioDeviceModule(); + XCTAssertNotEqual(secondAudioDeviceModule.get(), nullptr); + XCTAssertEqual(0, secondAudioDeviceModule->Init()); + + // Start playout for the default ADM but don't wait here. Instead use the + // upcoming second stream for that. We set the same expectation on number + // of callbacks as for the second stream. + mock.expectNeedMorePlayData(^int32_t(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void *audioSamples, + size_t &nSamplesOut, + int64_t *elapsed_time_ms, + int64_t *ntp_time_ms) { + nSamplesOut = nSamples; + XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer()); + XCTAssertEqual(nBytesPerSample, kBytesPerSample); + XCTAssertEqual(nChannels, self.playoutParameters.channels()); + XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate()); + XCTAssertNotEqual((void*)NULL, audioSamples); + + return 0; + }); + + XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock)); + [self startPlayout]; + + // Initialize playout for the second ADM. If all is OK, the second ADM shall + // reuse the audio session activated when the first ADM started playing. + // This call will also ensure that we avoid a problem related to initializing + // two different audio unit instances back to back (see webrtc:5166 for + // details). + XCTAssertEqual(0, secondAudioDeviceModule->InitPlayout()); + XCTAssertTrue(secondAudioDeviceModule->PlayoutIsInitialized()); + + // Start playout for the second ADM and verify that it starts as intended. + // Passing this test ensures that initialization of the second audio unit + // has been done successfully and that there is no conflict with the already + // playing first ADM. + XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"]; + __block int num_callbacks = 0; + + MockAudioTransport mock2; + mock2.expectNeedMorePlayData(^int32_t(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void *audioSamples, + size_t &nSamplesOut, + int64_t *elapsed_time_ms, + int64_t *ntp_time_ms) { + nSamplesOut = nSamples; + XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer()); + XCTAssertEqual(nBytesPerSample, kBytesPerSample); + XCTAssertEqual(nChannels, self.playoutParameters.channels()); + XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate()); + XCTAssertNotEqual((void*)NULL, audioSamples); + if (++num_callbacks == kNumCallbacks) { + [playoutExpectation fulfill]; + } + + return 0; + }); + + XCTAssertEqual(0, secondAudioDeviceModule->RegisterAudioCallback(&mock2)); + XCTAssertEqual(0, secondAudioDeviceModule->StartPlayout()); + XCTAssertTrue(secondAudioDeviceModule->Playing()); + [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil]; + [self stopPlayout]; + XCTAssertEqual(0, secondAudioDeviceModule->StopPlayout()); + XCTAssertFalse(secondAudioDeviceModule->Playing()); + XCTAssertFalse(secondAudioDeviceModule->PlayoutIsInitialized()); + + XCTAssertEqual(0, secondAudioDeviceModule->Terminate()); +} + +// Start playout and verify that the native audio layer starts asking for real +// audio samples to play out using the NeedMorePlayData callback. +- (void)testStartPlayoutVerifyCallbacks { + + XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"]; + __block int num_callbacks = 0; + mock.expectNeedMorePlayData(^int32_t(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void *audioSamples, + size_t &nSamplesOut, + int64_t *elapsed_time_ms, + int64_t *ntp_time_ms) { + nSamplesOut = nSamples; + XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer()); + XCTAssertEqual(nBytesPerSample, kBytesPerSample); + XCTAssertEqual(nChannels, self.playoutParameters.channels()); + XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate()); + XCTAssertNotEqual((void*)NULL, audioSamples); + if (++num_callbacks == kNumCallbacks) { + [playoutExpectation fulfill]; + } + return 0; + }); + + XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock)); + + [self startPlayout]; + [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil]; + [self stopPlayout]; +} + +// Start recording and verify that the native audio layer starts feeding real +// audio samples via the RecordedDataIsAvailable callback. +- (void)testStartRecordingVerifyCallbacks { + XCTestExpectation *recordExpectation = + [self expectationWithDescription:@"RecordedDataIsAvailable"]; + __block int num_callbacks = 0; + + mock.expectRecordedDataIsAvailable(^(const void* audioSamples, + const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + const uint32_t totalDelayMS, + const int32_t clockDrift, + const uint32_t currentMicLevel, + const bool keyPressed, + uint32_t& newMicLevel) { + XCTAssertNotEqual((void*)NULL, audioSamples); + XCTAssertEqual(nSamples, self.recordParameters.frames_per_10ms_buffer()); + XCTAssertEqual(nBytesPerSample, kBytesPerSample); + XCTAssertEqual(nChannels, self.recordParameters.channels()); + XCTAssertEqual((int)samplesPerSec, self.recordParameters.sample_rate()); + XCTAssertEqual(0, clockDrift); + XCTAssertEqual(0u, currentMicLevel); + XCTAssertFalse(keyPressed); + if (++num_callbacks == kNumCallbacks) { + [recordExpectation fulfill]; + } + + return 0; + }); + + XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock)); + [self startRecording]; + [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil]; + [self stopRecording]; +} + +// Start playout and recording (full-duplex audio) and verify that audio is +// active in both directions. +- (void)testStartPlayoutAndRecordingVerifyCallbacks { + XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"]; + __block NSUInteger callbackCount = 0; + + XCTestExpectation *recordExpectation = + [self expectationWithDescription:@"RecordedDataIsAvailable"]; + recordExpectation.expectedFulfillmentCount = kNumCallbacks; + + mock.expectNeedMorePlayData(^int32_t(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void *audioSamples, + size_t &nSamplesOut, + int64_t *elapsed_time_ms, + int64_t *ntp_time_ms) { + nSamplesOut = nSamples; + XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer()); + XCTAssertEqual(nBytesPerSample, kBytesPerSample); + XCTAssertEqual(nChannels, self.playoutParameters.channels()); + XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate()); + XCTAssertNotEqual((void*)NULL, audioSamples); + if (callbackCount++ >= kNumCallbacks) { + [playoutExpectation fulfill]; + } + + return 0; + }); + + mock.expectRecordedDataIsAvailable(^(const void* audioSamples, + const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + const uint32_t totalDelayMS, + const int32_t clockDrift, + const uint32_t currentMicLevel, + const bool keyPressed, + uint32_t& newMicLevel) { + XCTAssertNotEqual((void*)NULL, audioSamples); + XCTAssertEqual(nSamples, self.recordParameters.frames_per_10ms_buffer()); + XCTAssertEqual(nBytesPerSample, kBytesPerSample); + XCTAssertEqual(nChannels, self.recordParameters.channels()); + XCTAssertEqual((int)samplesPerSec, self.recordParameters.sample_rate()); + XCTAssertEqual(0, clockDrift); + XCTAssertEqual(0u, currentMicLevel); + XCTAssertFalse(keyPressed); + [recordExpectation fulfill]; + + return 0; + }); + + XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock)); + [self startPlayout]; + [self startRecording]; + [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil]; + [self stopRecording]; + [self stopPlayout]; +} + +// Start playout and read audio from an external PCM file when the audio layer +// asks for data to play out. Real audio is played out in this test but it does +// not contain any explicit verification that the audio quality is perfect. +- (void)testRunPlayoutWithFileAsSource { + XCTAssertEqual(1u, playoutParameters.channels()); + + // Using XCTestExpectation to count callbacks is very slow. + XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"]; + const int expectedCallbackCount = kFilePlayTimeInSec * kNumCallbacksPerSecond; + __block int callbackCount = 0; + + NSURL *fileURL = [self fileURLForSampleRate:playoutParameters.sample_rate()]; + NSInputStream *inputStream = [[NSInputStream alloc] initWithURL:fileURL]; + + mock.expectNeedMorePlayData(^int32_t(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void *audioSamples, + size_t &nSamplesOut, + int64_t *elapsed_time_ms, + int64_t *ntp_time_ms) { + [inputStream read:(uint8_t *)audioSamples maxLength:nSamples*nBytesPerSample*nChannels]; + nSamplesOut = nSamples; + if (callbackCount++ == expectedCallbackCount) { + [playoutExpectation fulfill]; + } + + return 0; + }); + + XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock)); + [self startPlayout]; + NSTimeInterval waitTimeout = kFilePlayTimeInSec * 2.0; + [self waitForExpectationsWithTimeout:waitTimeout handler:nil]; + [self stopPlayout]; +} + +- (void)testDevices { + // Device enumeration is not supported. Verify fixed values only. + XCTAssertEqual(1, audioDeviceModule->PlayoutDevices()); + XCTAssertEqual(1, audioDeviceModule->RecordingDevices()); +} + +// Start playout and recording and store recorded data in an intermediate FIFO +// buffer from which the playout side then reads its samples in the same order +// as they were stored. Under ideal circumstances, a callback sequence would +// look like: ...+-+-+-+-+-+-+-..., where '+' means 'packet recorded' and '-' +// means 'packet played'. Under such conditions, the FIFO would only contain +// one packet on average. However, under more realistic conditions, the size +// of the FIFO will vary more due to an unbalance between the two sides. +// This test tries to verify that the device maintains a balanced callback- +// sequence by running in loopback for ten seconds while measuring the size +// (max and average) of the FIFO. The size of the FIFO is increased by the +// recording side and decreased by the playout side. +// TODO(henrika): tune the final test parameters after running tests on several +// different devices. +- (void)testRunPlayoutAndRecordingInFullDuplex { + XCTAssertEqual(recordParameters.channels(), playoutParameters.channels()); + XCTAssertEqual(recordParameters.sample_rate(), playoutParameters.sample_rate()); + + XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"]; + __block NSUInteger playoutCallbacks = 0; + NSUInteger expectedPlayoutCallbacks = kFullDuplexTimeInSec * kNumCallbacksPerSecond; + + // FIFO queue and measurements + NSMutableArray *fifoBuffer = [NSMutableArray arrayWithCapacity:20]; + __block NSUInteger fifoMaxSize = 0; + __block NSUInteger fifoTotalWrittenElements = 0; + __block NSUInteger fifoWriteCount = 0; + + mock.expectRecordedDataIsAvailable(^(const void* audioSamples, + const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + const uint32_t totalDelayMS, + const int32_t clockDrift, + const uint32_t currentMicLevel, + const bool keyPressed, + uint32_t& newMicLevel) { + if (fifoWriteCount++ < kNumIgnoreFirstCallbacks) { + return 0; + } + + NSData *data = [NSData dataWithBytes:audioSamples length:nSamples*nBytesPerSample*nChannels]; + @synchronized(fifoBuffer) { + [fifoBuffer addObject:data]; + fifoMaxSize = MAX(fifoMaxSize, fifoBuffer.count); + fifoTotalWrittenElements += fifoBuffer.count; + } + + return 0; + }); + + mock.expectNeedMorePlayData(^int32_t(const size_t nSamples, + const size_t nBytesPerSample, + const size_t nChannels, + const uint32_t samplesPerSec, + void *audioSamples, + size_t &nSamplesOut, + int64_t *elapsed_time_ms, + int64_t *ntp_time_ms) { + nSamplesOut = nSamples; + NSData *data; + @synchronized(fifoBuffer) { + data = fifoBuffer.firstObject; + if (data) { + [fifoBuffer removeObjectAtIndex:0]; + } + } + + if (data) { + memcpy(audioSamples, (char*) data.bytes, data.length); + } else { + memset(audioSamples, 0, nSamples*nBytesPerSample*nChannels); + } + + if (playoutCallbacks++ == expectedPlayoutCallbacks) { + [playoutExpectation fulfill]; + } + return 0; + }); + + XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock)); + [self startRecording]; + [self startPlayout]; + NSTimeInterval waitTimeout = kFullDuplexTimeInSec * 2.0; + [self waitForExpectationsWithTimeout:waitTimeout handler:nil]; + + size_t fifoAverageSize = + (fifoTotalWrittenElements == 0) + ? 0.0 + : 0.5 + (double)fifoTotalWrittenElements / (fifoWriteCount - kNumIgnoreFirstCallbacks); + + [self stopPlayout]; + [self stopRecording]; + XCTAssertLessThan(fifoAverageSize, 10u); + XCTAssertLessThan(fifoMaxSize, 20u); +} + +@end |