/* * 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 #include #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 { bool _testEnabled; rtc::scoped_refptr 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]; #if defined(WEBRTC_IOS) && TARGET_OS_SIMULATOR // TODO(peterhanspers): Reenable these tests on simulator. // See bugs.webrtc.org/7812 _testEnabled = false; if (::getenv("WEBRTC_IOS_RUN_AUDIO_TESTS") != nullptr) { _testEnabled = true; } #else _testEnabled = true; #endif 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 { XCTSkipIf(!_testEnabled); // Using the test fixture to create and destruct the audio device module. } - (void)testInitTerminate { XCTSkipIf(!_testEnabled); // 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 { XCTSkipIf(!_testEnabled); [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 { XCTSkipIf(!_testEnabled); [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 { XCTSkipIf(!_testEnabled); 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 { XCTSkipIf(!_testEnabled); // Create and initialize a second/extra ADM instance. The default ADM is // created by the test harness. rtc::scoped_refptr 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 { XCTSkipIf(!_testEnabled); 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 { XCTSkipIf(!_testEnabled); 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 { XCTSkipIf(!_testEnabled); 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 { XCTSkipIf(!_testEnabled); 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 { XCTSkipIf(!_testEnabled); // 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 { XCTSkipIf(!_testEnabled); 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