webrtc/sdk/objc/unittests/RTCAudioDeviceModule_xctest.mm
Byoungchan Lee 2e631f5c38 Always build all iOS unittests, even on the simulator.
Also, make the iOS audio unittests not run on the simulator by default,
and if someone wants to run the tests one can do
by using the WEBRTC_IOS_RUN_AUDIO_TESTS environment variable.

Bug: webrtc:7812
Change-Id: Ie9fc70872c6617516e2f2c21039489df309b85fb
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/292621
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Commit-Queue: Daniel.L (Byoungchan) Lee <daniel.l@hpcnt.com>
Reviewed-by: Kári Helgason <kthelgason@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#39306}
2023-02-13 20:30:24 +00:00

618 lines
26 KiB
Text

/*
* 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>
#include <stdlib.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 {
bool _testEnabled;
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];
#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<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 {
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