| Index: webrtc/modules/audio_device/ios/audio_device_ios.mm
|
| diff --git a/webrtc/modules/audio_device/ios/audio_device_ios.mm b/webrtc/modules/audio_device/ios/audio_device_ios.mm
|
| index d7668f7894df500d385a50d959499ffa7911c091..e094ebdc3d833d1c0a12215cea6199bfb2e5802c 100644
|
| --- a/webrtc/modules/audio_device/ios/audio_device_ios.mm
|
| +++ b/webrtc/modules/audio_device/ios/audio_device_ios.mm
|
| @@ -19,12 +19,24 @@
|
|
|
| #include "webrtc/base/atomicops.h"
|
| #include "webrtc/base/checks.h"
|
| +#include "webrtc/base/criticalsection.h"
|
| #include "webrtc/base/logging.h"
|
| +#include "webrtc/base/thread_annotations.h"
|
| #include "webrtc/modules/audio_device/fine_audio_buffer.h"
|
| #include "webrtc/modules/utility/include/helpers_ios.h"
|
|
|
| namespace webrtc {
|
|
|
| +// Protects |g_audio_session_users|.
|
| +static rtc::GlobalLockPod g_lock;
|
| +
|
| +// Counts number of users (=instances of this object) who needs an active
|
| +// audio session. This variable is used to ensure that we only activate an audio
|
| +// session for the first user and deactivate it for the last.
|
| +// Member is static to ensure that the value is counted for all instances
|
| +// and not per instance.
|
| +static int g_audio_session_users GUARDED_BY(g_lock) = 0;
|
| +
|
| #define LOGI() LOG(LS_INFO) << "AudioDeviceIOS::"
|
|
|
| #define LOG_AND_RETURN_IF_ERROR(error, message) \
|
| @@ -77,10 +89,34 @@ const UInt16 kFixedRecordDelayEstimate = 30;
|
|
|
| using ios::CheckAndLogError;
|
|
|
| +// Verifies that the current audio session supports input audio and that the
|
| +// required category and mode are enabled.
|
| +static bool VerifyAudioSession(AVAudioSession* session) {
|
| + LOG(LS_INFO) << "VerifyAudioSession";
|
| + // Ensure that the device currently supports audio input.
|
| + if (!session.isInputAvailable) {
|
| + LOG(LS_ERROR) << "No audio input path is available!";
|
| + return false;
|
| + }
|
| +
|
| + // Ensure that the required category and mode are actually activated.
|
| + if (![session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) {
|
| + LOG(LS_ERROR)
|
| + << "Failed to set category to AVAudioSessionCategoryPlayAndRecord";
|
| + return false;
|
| + }
|
| + if (![session.mode isEqualToString:AVAudioSessionModeVoiceChat]) {
|
| + LOG(LS_ERROR) << "Failed to set mode to AVAudioSessionModeVoiceChat";
|
| + return false;
|
| + }
|
| + return true;
|
| +}
|
| +
|
| // Activates an audio session suitable for full duplex VoIP sessions when
|
| // |activate| is true. Also sets the preferred sample rate and IO buffer
|
| // duration. Deactivates an active audio session if |activate| is set to false.
|
| -static void ActivateAudioSession(AVAudioSession* session, bool activate) {
|
| +static bool ActivateAudioSession(AVAudioSession* session, bool activate)
|
| + EXCLUSIVE_LOCKS_REQUIRED(g_lock) {
|
| LOG(LS_INFO) << "ActivateAudioSession(" << activate << ")";
|
| @autoreleasepool {
|
| NSError* error = nil;
|
| @@ -96,8 +132,7 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
|
| setActive:NO
|
| withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
|
| error:&error];
|
| - RTC_DCHECK(CheckAndLogError(success, error));
|
| - return;
|
| + return CheckAndLogError(success, error);
|
| }
|
|
|
| // Go ahead and active our own audio session since |activate| is true.
|
| @@ -129,7 +164,6 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
|
| RTC_DCHECK(CheckAndLogError(success, error));
|
|
|
| // Set the preferred audio I/O buffer duration, in seconds.
|
| - // TODO(henrika): add more comments here.
|
| error = nil;
|
| success = [session setPreferredIOBufferDuration:kPreferredIOBufferDuration
|
| error:&error];
|
| @@ -139,13 +173,15 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
|
| // session (e.g. phone call) has higher priority than ours.
|
| error = nil;
|
| success = [session setActive:YES error:&error];
|
| - RTC_DCHECK(CheckAndLogError(success, error));
|
| - RTC_CHECK(session.isInputAvailable) << "No input path is available!";
|
| + if (!CheckAndLogError(success, error)) {
|
| + return false;
|
| + }
|
|
|
| - // Ensure that category and mode are actually activated.
|
| - RTC_DCHECK(
|
| - [session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]);
|
| - RTC_DCHECK([session.mode isEqualToString:AVAudioSessionModeVoiceChat]);
|
| + // Ensure that the active audio session has the correct category and mode.
|
| + if (!VerifyAudioSession(session)) {
|
| + LOG(LS_ERROR) << "Failed to verify audio session category and mode";
|
| + return false;
|
| + }
|
|
|
| // Try to set the preferred number of hardware audio channels. These calls
|
| // must be done after setting the audio session’s category and mode and
|
| @@ -164,9 +200,54 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
|
| [session setPreferredOutputNumberOfChannels:kPreferredNumberOfChannels
|
| error:&error];
|
| RTC_DCHECK(CheckAndLogError(success, error));
|
| + return true;
|
| }
|
| }
|
|
|
| +// An application can create more than one ADM and start audio streaming
|
| +// for all of them. It is essential that we only activate the app's audio
|
| +// session once (for the first one) and deactivate it once (for the last).
|
| +static bool ActivateAudioSession() {
|
| + LOGI() << "ActivateAudioSession";
|
| + rtc::GlobalLockScope ls(&g_lock);
|
| + if (g_audio_session_users == 0) {
|
| + // The system provides an audio session object upon launch of an
|
| + // application. However, we must initialize the session in order to
|
| + // handle interruptions. Implicit initialization occurs when obtaining
|
| + // a reference to the AVAudioSession object.
|
| + AVAudioSession* session = [AVAudioSession sharedInstance];
|
| + // Try to activate the audio session and ask for a set of preferred audio
|
| + // parameters.
|
| + if (!ActivateAudioSession(session, true)) {
|
| + LOG(LS_ERROR) << "Failed to activate the audio session";
|
| + return false;
|
| + }
|
| + LOG(LS_INFO) << "The audio session is now activated";
|
| + }
|
| + ++g_audio_session_users;
|
| + LOG(LS_INFO) << "Number of audio session users: " << g_audio_session_users;
|
| + return true;
|
| +}
|
| +
|
| +// If more than one object is using the audio session, ensure that only the
|
| +// last object deactivates. Apple recommends: "activate your audio session
|
| +// only as needed and deactivate it when you are not using audio".
|
| +static bool DeactivateAudioSession() {
|
| + LOGI() << "DeactivateAudioSession";
|
| + rtc::GlobalLockScope ls(&g_lock);
|
| + if (g_audio_session_users == 1) {
|
| + AVAudioSession* session = [AVAudioSession sharedInstance];
|
| + if (!ActivateAudioSession(session, false)) {
|
| + LOG(LS_ERROR) << "Failed to deactivate the audio session";
|
| + return false;
|
| + }
|
| + LOG(LS_INFO) << "Our audio session is now deactivated";
|
| + }
|
| + --g_audio_session_users;
|
| + LOG(LS_INFO) << "Number of audio session users: " << g_audio_session_users;
|
| + return true;
|
| +}
|
| +
|
| #if !defined(NDEBUG)
|
| // Helper method for printing out an AudioStreamBasicDescription structure.
|
| static void LogABSD(AudioStreamBasicDescription absd) {
|
| @@ -212,7 +293,7 @@ AudioDeviceIOS::AudioDeviceIOS()
|
| }
|
|
|
| AudioDeviceIOS::~AudioDeviceIOS() {
|
| - LOGI() << "~dtor";
|
| + LOGI() << "~dtor" << ios::GetCurrentThreadDescription();
|
| RTC_DCHECK(thread_checker_.CalledOnValidThread());
|
| Terminate();
|
| }
|
| @@ -254,8 +335,16 @@ int32_t AudioDeviceIOS::Terminate() {
|
| if (!initialized_) {
|
| return 0;
|
| }
|
| - ShutdownPlayOrRecord();
|
| + StopPlayout();
|
| + StopRecording();
|
| initialized_ = false;
|
| + {
|
| + rtc::GlobalLockScope ls(&g_lock);
|
| + if (g_audio_session_users != 0) {
|
| + LOG(LS_WARNING) << "Object is destructed with an active audio session";
|
| + }
|
| + RTC_DCHECK_GE(g_audio_session_users, 0);
|
| + }
|
| return 0;
|
| }
|
|
|
| @@ -267,7 +356,7 @@ int32_t AudioDeviceIOS::InitPlayout() {
|
| RTC_DCHECK(!playing_);
|
| if (!rec_is_initialized_) {
|
| if (!InitPlayOrRecord()) {
|
| - LOG_F(LS_ERROR) << "InitPlayOrRecord failed!";
|
| + LOG_F(LS_ERROR) << "InitPlayOrRecord failed for InitPlayout!";
|
| return -1;
|
| }
|
| }
|
| @@ -283,7 +372,7 @@ int32_t AudioDeviceIOS::InitRecording() {
|
| RTC_DCHECK(!recording_);
|
| if (!play_is_initialized_) {
|
| if (!InitPlayOrRecord()) {
|
| - LOG_F(LS_ERROR) << "InitPlayOrRecord failed!";
|
| + LOG_F(LS_ERROR) << "InitPlayOrRecord failed for InitRecording!";
|
| return -1;
|
| }
|
| }
|
| @@ -300,9 +389,11 @@ int32_t AudioDeviceIOS::StartPlayout() {
|
| if (!recording_) {
|
| OSStatus result = AudioOutputUnitStart(vpio_unit_);
|
| if (result != noErr) {
|
| - LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result;
|
| + LOG_F(LS_ERROR) << "AudioOutputUnitStart failed for StartPlayout: "
|
| + << result;
|
| return -1;
|
| }
|
| + LOG(LS_INFO) << "Voice-Processing I/O audio unit is now started";
|
| }
|
| rtc::AtomicOps::ReleaseStore(&playing_, 1);
|
| return 0;
|
| @@ -331,9 +422,11 @@ int32_t AudioDeviceIOS::StartRecording() {
|
| if (!playing_) {
|
| OSStatus result = AudioOutputUnitStart(vpio_unit_);
|
| if (result != noErr) {
|
| - LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result;
|
| + LOG_F(LS_ERROR) << "AudioOutputUnitStart failed for StartRecording: "
|
| + << result;
|
| return -1;
|
| }
|
| + LOG(LS_INFO) << "Voice-Processing I/O audio unit is now started";
|
| }
|
| rtc::AtomicOps::ReleaseStore(&recording_, 1);
|
| return 0;
|
| @@ -639,7 +732,7 @@ void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() {
|
|
|
| bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
|
| LOGI() << "SetupAndInitializeVoiceProcessingAudioUnit";
|
| - RTC_DCHECK(!vpio_unit_);
|
| + RTC_DCHECK(!vpio_unit_) << "VoiceProcessingIO audio unit already exists";
|
| // Create an audio component description to identify the Voice-Processing
|
| // I/O audio unit.
|
| AudioComponentDescription vpio_unit_description;
|
| @@ -653,29 +746,42 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
|
| AudioComponentFindNext(nullptr, &vpio_unit_description);
|
|
|
| // Create a Voice-Processing IO audio unit.
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioComponentInstanceNew(found_vpio_unit_ref, &vpio_unit_),
|
| - "Failed to create a VoiceProcessingIO audio unit");
|
| + OSStatus result = noErr;
|
| + result = AudioComponentInstanceNew(found_vpio_unit_ref, &vpio_unit_);
|
| + if (result != noErr) {
|
| + vpio_unit_ = nullptr;
|
| + LOG(LS_ERROR) << "AudioComponentInstanceNew failed: " << result;
|
| + return false;
|
| + }
|
|
|
| // A VP I/O unit's bus 1 connects to input hardware (microphone). Enable
|
| // input on the input scope of the input element.
|
| AudioUnitElement input_bus = 1;
|
| UInt32 enable_input = 1;
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
|
| - kAudioUnitScope_Input, input_bus, &enable_input,
|
| - sizeof(enable_input)),
|
| - "Failed to enable input on input scope of input element");
|
| + result = AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
|
| + kAudioUnitScope_Input, input_bus, &enable_input,
|
| + sizeof(enable_input));
|
| + if (result != noErr) {
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR) << "Failed to enable input on input scope of input element: "
|
| + << result;
|
| + return false;
|
| + }
|
|
|
| // A VP I/O unit's bus 0 connects to output hardware (speaker). Enable
|
| // output on the output scope of the output element.
|
| AudioUnitElement output_bus = 0;
|
| UInt32 enable_output = 1;
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
|
| - kAudioUnitScope_Output, output_bus, &enable_output,
|
| - sizeof(enable_output)),
|
| - "Failed to enable output on output scope of output element");
|
| + result = AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
|
| + kAudioUnitScope_Output, output_bus,
|
| + &enable_output, sizeof(enable_output));
|
| + if (result != noErr) {
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR)
|
| + << "Failed to enable output on output scope of output element: "
|
| + << result;
|
| + return false;
|
| + }
|
|
|
| // Set the application formats for input and output:
|
| // - use same format in both directions
|
| @@ -703,38 +809,55 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
|
| #endif
|
|
|
| // Set the application format on the output scope of the input element/bus.
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
|
| - kAudioUnitScope_Output, input_bus,
|
| - &application_format, size),
|
| - "Failed to set application format on output scope of input element");
|
| + result = AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
|
| + kAudioUnitScope_Output, input_bus,
|
| + &application_format, size);
|
| + if (result != noErr) {
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR)
|
| + << "Failed to set application format on output scope of input bus: "
|
| + << result;
|
| + return false;
|
| + }
|
|
|
| // Set the application format on the input scope of the output element/bus.
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
|
| - kAudioUnitScope_Input, output_bus,
|
| - &application_format, size),
|
| - "Failed to set application format on input scope of output element");
|
| + result = AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
|
| + kAudioUnitScope_Input, output_bus,
|
| + &application_format, size);
|
| + if (result != noErr) {
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR)
|
| + << "Failed to set application format on input scope of output bus: "
|
| + << result;
|
| + return false;
|
| + }
|
|
|
| // Specify the callback function that provides audio samples to the audio
|
| // unit.
|
| AURenderCallbackStruct render_callback;
|
| render_callback.inputProc = GetPlayoutData;
|
| render_callback.inputProcRefCon = this;
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_SetRenderCallback,
|
| - kAudioUnitScope_Input, output_bus, &render_callback,
|
| - sizeof(render_callback)),
|
| - "Failed to specify the render callback on the output element");
|
| + result = AudioUnitSetProperty(
|
| + vpio_unit_, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input,
|
| + output_bus, &render_callback, sizeof(render_callback));
|
| + if (result != noErr) {
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR) << "Failed to specify the render callback on the output bus: "
|
| + << result;
|
| + return false;
|
| + }
|
|
|
| // Disable AU buffer allocation for the recorder, we allocate our own.
|
| // TODO(henrika): not sure that it actually saves resource to make this call.
|
| UInt32 flag = 0;
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_ShouldAllocateBuffer,
|
| - kAudioUnitScope_Output, input_bus, &flag,
|
| - sizeof(flag)),
|
| - "Failed to disable buffer allocation on the input element");
|
| + result = AudioUnitSetProperty(
|
| + vpio_unit_, kAudioUnitProperty_ShouldAllocateBuffer,
|
| + kAudioUnitScope_Output, input_bus, &flag, sizeof(flag));
|
| + if (result != noErr) {
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR) << "Failed to disable buffer allocation on the input bus: "
|
| + << result;
|
| + }
|
|
|
| // Specify the callback to be called by the I/O thread to us when input audio
|
| // is available. The recorded samples can then be obtained by calling the
|
| @@ -742,16 +865,28 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
|
| AURenderCallbackStruct input_callback;
|
| input_callback.inputProc = RecordedDataIsAvailable;
|
| input_callback.inputProcRefCon = this;
|
| - LOG_AND_RETURN_IF_ERROR(
|
| - AudioUnitSetProperty(vpio_unit_,
|
| - kAudioOutputUnitProperty_SetInputCallback,
|
| - kAudioUnitScope_Global, input_bus, &input_callback,
|
| - sizeof(input_callback)),
|
| - "Failed to specify the input callback on the input element");
|
| + result = AudioUnitSetProperty(vpio_unit_,
|
| + kAudioOutputUnitProperty_SetInputCallback,
|
| + kAudioUnitScope_Global, input_bus,
|
| + &input_callback, sizeof(input_callback));
|
| + if (result != noErr) {
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR) << "Failed to specify the input callback on the input bus: "
|
| + << result;
|
| + }
|
|
|
| // Initialize the Voice-Processing I/O unit instance.
|
| - LOG_AND_RETURN_IF_ERROR(AudioUnitInitialize(vpio_unit_),
|
| - "Failed to initialize the Voice-Processing I/O unit");
|
| + result = AudioUnitInitialize(vpio_unit_);
|
| + if (result != noErr) {
|
| + result = AudioUnitUninitialize(vpio_unit_);
|
| + if (result != noErr) {
|
| + LOG_F(LS_ERROR) << "AudioUnitUninitialize failed: " << result;
|
| + }
|
| + DisposeAudioUnit();
|
| + LOG(LS_ERROR) << "Failed to initialize the Voice-Processing I/O unit: "
|
| + << result;
|
| + return false;
|
| + }
|
| return true;
|
| }
|
|
|
| @@ -790,9 +925,18 @@ bool AudioDeviceIOS::RestartAudioUnitWithNewFormat(float sample_rate) {
|
|
|
| bool AudioDeviceIOS::InitPlayOrRecord() {
|
| LOGI() << "InitPlayOrRecord";
|
| + // Activate the audio session if not already activated.
|
| + if (!ActivateAudioSession()) {
|
| + return false;
|
| + }
|
| +
|
| + // Ensure that the active audio session has the correct category and mode.
|
| AVAudioSession* session = [AVAudioSession sharedInstance];
|
| - // Activate the audio session and ask for a set of preferred audio parameters.
|
| - ActivateAudioSession(session, true);
|
| + if (!VerifyAudioSession(session)) {
|
| + DeactivateAudioSession();
|
| + LOG(LS_ERROR) << "Failed to verify audio session category and mode";
|
| + return false;
|
| + }
|
|
|
| // Start observing audio session interruptions and route changes.
|
| RegisterNotificationObservers();
|
| @@ -802,16 +946,16 @@ bool AudioDeviceIOS::InitPlayOrRecord() {
|
|
|
| // Create, setup and initialize a new Voice-Processing I/O unit.
|
| if (!SetupAndInitializeVoiceProcessingAudioUnit()) {
|
| + // Reduce usage count for the audio session and possibly deactivate it if
|
| + // this object is the only user.
|
| + DeactivateAudioSession();
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| -bool AudioDeviceIOS::ShutdownPlayOrRecord() {
|
| +void AudioDeviceIOS::ShutdownPlayOrRecord() {
|
| LOGI() << "ShutdownPlayOrRecord";
|
| - // Remove audio session notification observers.
|
| - UnregisterNotificationObservers();
|
| -
|
| // Close and delete the voice-processing I/O unit.
|
| OSStatus result = -1;
|
| if (nullptr != vpio_unit_) {
|
| @@ -823,18 +967,25 @@ bool AudioDeviceIOS::ShutdownPlayOrRecord() {
|
| if (result != noErr) {
|
| LOG_F(LS_ERROR) << "AudioUnitUninitialize failed: " << result;
|
| }
|
| - result = AudioComponentInstanceDispose(vpio_unit_);
|
| - if (result != noErr) {
|
| - LOG_F(LS_ERROR) << "AudioComponentInstanceDispose failed: " << result;
|
| - }
|
| - vpio_unit_ = nullptr;
|
| + DisposeAudioUnit();
|
| }
|
|
|
| + // Remove audio session notification observers.
|
| + UnregisterNotificationObservers();
|
| +
|
| // All I/O should be stopped or paused prior to deactivating the audio
|
| // session, hence we deactivate as last action.
|
| - AVAudioSession* session = [AVAudioSession sharedInstance];
|
| - ActivateAudioSession(session, false);
|
| - return true;
|
| + DeactivateAudioSession();
|
| +}
|
| +
|
| +void AudioDeviceIOS::DisposeAudioUnit() {
|
| + if (nullptr == vpio_unit_)
|
| + return;
|
| + OSStatus result = AudioComponentInstanceDispose(vpio_unit_);
|
| + if (result != noErr) {
|
| + LOG(LS_ERROR) << "AudioComponentInstanceDispose failed:" << result;
|
| + }
|
| + vpio_unit_ = nullptr;
|
| }
|
|
|
| OSStatus AudioDeviceIOS::RecordedDataIsAvailable(
|
|
|