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 9db9871c359b60820813b00131712c366c35c818..f26e9f1cc7463657a2646aa28f4789cc9bbe73b2 100644 |
--- a/webrtc/modules/audio_device/ios/audio_device_ios.mm |
+++ b/webrtc/modules/audio_device/ios/audio_device_ios.mm |
@@ -36,6 +36,14 @@ namespace webrtc { |
} \ |
} while (0) |
+#define LOG_IF_ERROR(error, message) \ |
+ do { \ |
+ OSStatus err = error; \ |
+ if (err) { \ |
+ LOG(LS_ERROR) << message << ": " << err; \ |
+ } \ |
+ } while (0) |
+ |
// Preferred hardware sample rate (unit is in Hertz). The client sample rate |
// will be set to this value as well to avoid resampling the the audio unit's |
// format converter. Note that, some devices, e.g. BT headsets, only supports |
@@ -77,12 +85,14 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) { |
@autoreleasepool { |
NSError* error = nil; |
BOOL success = NO; |
+ |
// Deactivate the audio session and return if |activate| is false. |
if (!activate) { |
success = [session setActive:NO error:&error]; |
RTC_DCHECK(CheckAndLogError(success, error)); |
return; |
} |
+ |
// Use a category which supports simultaneous recording and playback. |
// By default, using this category implies that our app’s audio is |
// nonmixable, hence activating the session will interrupt any other |
@@ -90,15 +100,18 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) { |
if (session.category != AVAudioSessionCategoryPlayAndRecord) { |
error = nil; |
success = [session setCategory:AVAudioSessionCategoryPlayAndRecord |
+ withOptions:AVAudioSessionCategoryOptionAllowBluetooth |
error:&error]; |
RTC_DCHECK(CheckAndLogError(success, error)); |
} |
+ |
// Specify mode for two-way voice communication (e.g. VoIP). |
if (session.mode != AVAudioSessionModeVoiceChat) { |
error = nil; |
success = [session setMode:AVAudioSessionModeVoiceChat error:&error]; |
RTC_DCHECK(CheckAndLogError(success, error)); |
} |
+ |
// Set the session's sample rate or the hardware sample rate. |
// It is essential that we use the same sample rate as stream format |
// to ensure that the I/O unit does not have to do sample rate conversion. |
@@ -106,6 +119,7 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) { |
success = |
[session setPreferredSampleRate:kPreferredSampleRate error:&error]; |
RTC_DCHECK(CheckAndLogError(success, error)); |
+ |
// Set the preferred audio I/O buffer duration, in seconds. |
// TODO(henrika): add more comments here. |
error = nil; |
@@ -113,18 +127,18 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) { |
error:&error]; |
RTC_DCHECK(CheckAndLogError(success, error)); |
- // TODO(henrika): add observers here... |
- |
// Activate the audio session. Activation can fail if another active audio |
// 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!"; |
+ |
// Ensure that category and mode are actually activated. |
RTC_DCHECK( |
[session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]); |
RTC_DCHECK([session.mode isEqualToString:AVAudioSessionModeVoiceChat]); |
+ |
// 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 |
// activating the session. |
@@ -404,22 +418,163 @@ void AudioDeviceIOS::UpdateAudioDeviceBuffer() { |
audio_device_buffer_->SetRecordingChannels(record_parameters_.channels()); |
} |
+void AudioDeviceIOS::RegisterNotificationObservers() { |
+ LOGI() << "RegisterNotificationObservers"; |
+ // This code block will be called when AVAudioSessionInterruptionNotification |
+ // is observed. |
+ void (^interrupt_block)(NSNotification*) = ^(NSNotification* notification) { |
+ NSNumber* type_number = |
+ notification.userInfo[AVAudioSessionInterruptionTypeKey]; |
+ AVAudioSessionInterruptionType type = |
+ (AVAudioSessionInterruptionType)type_number.unsignedIntegerValue; |
+ LOG(LS_INFO) << "Audio session interruption:"; |
+ switch (type) { |
+ case AVAudioSessionInterruptionTypeBegan: |
+ // The system has deactivated our audio session. |
+ // Stop the active audio unit. |
+ LOG(LS_INFO) << " Began => stopping the audio unit"; |
+ LOG_IF_ERROR(AudioOutputUnitStop(vpio_unit_), |
+ "Failed to stop the the Voice-Processing I/O unit"); |
+ break; |
+ case AVAudioSessionInterruptionTypeEnded: |
+ // The interruption has ended. Restart the audio session and start the |
+ // initialized audio unit again. |
+ LOG(LS_INFO) << " Ended => restarting audio session and audio unit"; |
+ NSError* error = nil; |
+ BOOL success = NO; |
+ AVAudioSession* session = [AVAudioSession sharedInstance]; |
+ success = [session setActive:YES error:&error]; |
+ if (CheckAndLogError(success, error)) { |
+ LOG_IF_ERROR(AudioOutputUnitStart(vpio_unit_), |
+ "Failed to start the the Voice-Processing I/O unit"); |
+ } |
+ break; |
+ } |
+ }; |
+ |
+ // This code block will be called when AVAudioSessionRouteChangeNotification |
+ // is observed. |
+ void (^route_change_block)(NSNotification*) = |
+ ^(NSNotification* notification) { |
+ // Get reason for current route change. |
+ NSNumber* reason_number = |
+ notification.userInfo[AVAudioSessionRouteChangeReasonKey]; |
+ AVAudioSessionRouteChangeReason reason = |
+ (AVAudioSessionRouteChangeReason)reason_number.unsignedIntegerValue; |
+ bool valid_route_change = true; |
+ LOG(LS_INFO) << "Route change:"; |
+ switch (reason) { |
+ case AVAudioSessionRouteChangeReasonUnknown: |
+ LOG(LS_INFO) << " ReasonUnknown"; |
+ break; |
+ case AVAudioSessionRouteChangeReasonNewDeviceAvailable: |
+ LOG(LS_INFO) << " NewDeviceAvailable"; |
+ break; |
+ case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: |
+ LOG(LS_INFO) << " OldDeviceUnavailable"; |
+ break; |
+ case AVAudioSessionRouteChangeReasonCategoryChange: |
+ LOG(LS_INFO) << " CategoryChange"; |
+ LOG(LS_INFO) << " New category: " << ios::GetAudioSessionCategory(); |
+ // Don't see this as route change since it can be triggered in |
+ // combination with session interruptions as well. |
+ valid_route_change = false; |
+ break; |
+ case AVAudioSessionRouteChangeReasonOverride: |
+ LOG(LS_INFO) << " Override"; |
+ break; |
+ case AVAudioSessionRouteChangeReasonWakeFromSleep: |
+ LOG(LS_INFO) << " WakeFromSleep"; |
+ break; |
+ case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory: |
+ LOG(LS_INFO) << " NoSuitableRouteForCategory"; |
+ break; |
+ case AVAudioSessionRouteChangeReasonRouteConfigurationChange: |
+ // Ignore this type of route change since we are focusing |
+ // on detecting headset changes. |
+ LOG(LS_INFO) << " RouteConfigurationChange"; |
+ valid_route_change = false; |
+ break; |
+ } |
+ |
+ if (valid_route_change) { |
+ // Log previous route configuration. |
+ AVAudioSessionRouteDescription* prev_route = |
+ notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey]; |
+ LOG(LS_INFO) << "Previous route:"; |
+ LOG(LS_INFO) << ios::StdStringFromNSString( |
+ [NSString stringWithFormat:@"%@", prev_route]); |
+ |
+ // Only restart audio for a valid route change and if the |
+ // session sample rate has changed. |
+ AVAudioSession* session = [AVAudioSession sharedInstance]; |
+ const double session_sample_rate = session.sampleRate; |
+ LOG(LS_INFO) << "session sample rate: " << session_sample_rate; |
+ if (playout_parameters_.sample_rate() != session_sample_rate) { |
+ if (!RestartAudioUnitWithNewFormat(session_sample_rate)) { |
+ LOG(LS_ERROR) << "Audio restart failed"; |
+ } |
+ } |
+ } |
+ }; |
+ |
+ // Get the default notification center of the current process. |
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
+ |
+ // Add AVAudioSessionInterruptionNotification observer. |
+ id interruption_observer = |
+ [center addObserverForName:AVAudioSessionInterruptionNotification |
+ object:nil |
+ queue:[NSOperationQueue mainQueue] |
+ usingBlock:interrupt_block]; |
+ // Add AVAudioSessionRouteChangeNotification observer. |
+ id route_change_observer = |
+ [center addObserverForName:AVAudioSessionRouteChangeNotification |
+ object:nil |
+ queue:[NSOperationQueue mainQueue] |
+ usingBlock:route_change_block]; |
+ |
+ // Increment refcount on observers using ARC bridge. Instance variable is a |
+ // void* instead of an id because header is included in other pure C++ |
+ // files. |
+ audio_interruption_observer_ = (__bridge_retained void*)interruption_observer; |
+ route_change_observer_ = (__bridge_retained void*)route_change_observer; |
+} |
+ |
+void AudioDeviceIOS::UnregisterNotificationObservers() { |
+ LOGI() << "UnregisterNotificationObservers"; |
+ // Transfer ownership of observer back to ARC, which will deallocate the |
+ // observer once it exits this scope. |
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
+ if (audio_interruption_observer_ != nullptr) { |
+ id observer = (__bridge_transfer id)audio_interruption_observer_; |
+ [center removeObserver:observer]; |
+ audio_interruption_observer_ = nullptr; |
+ } |
+ if (route_change_observer_ != nullptr) { |
+ id observer = (__bridge_transfer id)route_change_observer_; |
+ [center removeObserver:observer]; |
+ route_change_observer_ = nullptr; |
+ } |
+} |
+ |
void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() { |
LOGI() << "SetupAudioBuffersForActiveAudioSession"; |
- AVAudioSession* session = [AVAudioSession sharedInstance]; |
// Verify the current values once the audio session has been activated. |
+ AVAudioSession* session = [AVAudioSession sharedInstance]; |
LOG(LS_INFO) << " sample rate: " << session.sampleRate; |
LOG(LS_INFO) << " IO buffer duration: " << session.IOBufferDuration; |
LOG(LS_INFO) << " output channels: " << session.outputNumberOfChannels; |
LOG(LS_INFO) << " input channels: " << session.inputNumberOfChannels; |
LOG(LS_INFO) << " output latency: " << session.outputLatency; |
LOG(LS_INFO) << " input latency: " << session.inputLatency; |
+ |
// Log a warning message for the case when we are unable to set the preferred |
// hardware sample rate but continue and use the non-ideal sample rate after |
- // reinitializing the audio parameters. |
- if (session.sampleRate != playout_parameters_.sample_rate()) { |
- LOG(LS_WARNING) |
- << "Failed to enable an audio session with the preferred sample rate!"; |
+ // reinitializing the audio parameters. Most BT headsets only support 8kHz or |
+ // 16kHz. |
+ if (session.sampleRate != kPreferredSampleRate) { |
+ LOG(LS_WARNING) << "Unable to set the preferred sample rate"; |
} |
// At this stage, we also know the exact IO buffer duration and can add |
@@ -532,8 +687,10 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() { |
application_format.mBytesPerFrame = kBytesPerSample; |
application_format.mChannelsPerFrame = kPreferredNumberOfChannels; |
application_format.mBitsPerChannel = 8 * kBytesPerSample; |
+ // Store the new format. |
+ application_format_ = application_format; |
#if !defined(NDEBUG) |
- LogABSD(application_format); |
+ LogABSD(application_format_); |
#endif |
// Set the application format on the output scope of the input element/bus. |
@@ -589,12 +746,48 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() { |
return true; |
} |
+bool AudioDeviceIOS::RestartAudioUnitWithNewFormat(float sample_rate) { |
+ LOGI() << "RestartAudioUnitWithNewFormat(sample_rate=" << sample_rate << ")"; |
+ // Stop the active audio unit. |
+ LOG_AND_RETURN_IF_ERROR(AudioOutputUnitStop(vpio_unit_), |
+ "Failed to stop the the Voice-Processing I/O unit"); |
+ |
+ // The stream format is about to be changed and it requires that we first |
+ // uninitialize it to deallocate its resources. |
+ LOG_AND_RETURN_IF_ERROR( |
+ AudioUnitUninitialize(vpio_unit_), |
+ "Failed to uninitialize the the Voice-Processing I/O unit"); |
+ |
+ // Allocate new buffers given the new stream format. |
+ SetupAudioBuffersForActiveAudioSession(); |
+ |
+ // Update the existing application format using the new sample rate. |
+ application_format_.mSampleRate = playout_parameters_.sample_rate(); |
+ UInt32 size = sizeof(application_format_); |
+ AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat, |
+ kAudioUnitScope_Output, 1, &application_format_, size); |
+ AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat, |
+ kAudioUnitScope_Input, 0, &application_format_, size); |
+ |
+ // Prepare the audio unit to render audio again. |
+ LOG_AND_RETURN_IF_ERROR(AudioUnitInitialize(vpio_unit_), |
+ "Failed to initialize the Voice-Processing I/O unit"); |
+ |
+ // Start rendering audio using the new format. |
+ LOG_AND_RETURN_IF_ERROR(AudioOutputUnitStart(vpio_unit_), |
+ "Failed to start the Voice-Processing I/O unit"); |
+ return true; |
+} |
+ |
bool AudioDeviceIOS::InitPlayOrRecord() { |
LOGI() << "InitPlayOrRecord"; |
AVAudioSession* session = [AVAudioSession sharedInstance]; |
// Activate the audio session and ask for a set of preferred audio parameters. |
ActivateAudioSession(session, true); |
+ // Start observing audio session interruptions and route changes. |
+ RegisterNotificationObservers(); |
+ |
// Ensure that we got what what we asked for in our active audio session. |
SetupAudioBuffersForActiveAudioSession(); |
@@ -602,59 +795,14 @@ bool AudioDeviceIOS::InitPlayOrRecord() { |
if (!SetupAndInitializeVoiceProcessingAudioUnit()) { |
return false; |
} |
- |
- // Listen to audio interruptions. |
- // TODO(henrika): learn this area better. |
- NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
- id observer = [center |
- addObserverForName:AVAudioSessionInterruptionNotification |
- object:nil |
- queue:[NSOperationQueue mainQueue] |
- usingBlock:^(NSNotification* notification) { |
- NSNumber* typeNumber = |
- [notification userInfo][AVAudioSessionInterruptionTypeKey]; |
- AVAudioSessionInterruptionType type = |
- (AVAudioSessionInterruptionType)[typeNumber |
- unsignedIntegerValue]; |
- switch (type) { |
- case AVAudioSessionInterruptionTypeBegan: |
- // At this point our audio session has been deactivated and |
- // the audio unit render callbacks no longer occur. |
- // Nothing to do. |
- break; |
- case AVAudioSessionInterruptionTypeEnded: { |
- NSError* error = nil; |
- AVAudioSession* session = [AVAudioSession sharedInstance]; |
- [session setActive:YES error:&error]; |
- if (error != nil) { |
- LOG_F(LS_ERROR) << "Failed to active audio session"; |
- } |
- // Post interruption the audio unit render callbacks don't |
- // automatically continue, so we restart the unit manually |
- // here. |
- AudioOutputUnitStop(vpio_unit_); |
- AudioOutputUnitStart(vpio_unit_); |
- break; |
- } |
- } |
- }]; |
- // Increment refcount on observer using ARC bridge. Instance variable is a |
- // void* instead of an id because header is included in other pure C++ |
- // files. |
- audio_interruption_observer_ = (__bridge_retained void*)observer; |
return true; |
} |
bool AudioDeviceIOS::ShutdownPlayOrRecord() { |
LOGI() << "ShutdownPlayOrRecord"; |
- if (audio_interruption_observer_ != nullptr) { |
- NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
- // Transfer ownership of observer back to ARC, which will dealloc the |
- // observer once it exits this scope. |
- id observer = (__bridge_transfer id)audio_interruption_observer_; |
- [center removeObserver:observer]; |
- audio_interruption_observer_ = nullptr; |
- } |
+ // Remove audio session notification observers. |
+ UnregisterNotificationObservers(); |
+ |
// Close and delete the voice-processing I/O unit. |
OSStatus result = -1; |
if (nullptr != vpio_unit_) { |
@@ -662,12 +810,17 @@ bool AudioDeviceIOS::ShutdownPlayOrRecord() { |
if (result != noErr) { |
LOG_F(LS_ERROR) << "AudioOutputUnitStop failed: " << result; |
} |
+ result = AudioUnitUninitialize(vpio_unit_); |
+ 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; |
} |
+ |
// All I/O should be stopped or paused prior to deactivating the audio |
// session, hence we deactivate as last action. |
AVAudioSession* session = [AVAudioSession sharedInstance]; |
@@ -695,12 +848,16 @@ OSStatus AudioDeviceIOS::OnRecordedDataIsAvailable( |
const AudioTimeStamp* in_time_stamp, |
UInt32 in_bus_number, |
UInt32 in_number_frames) { |
- RTC_DCHECK_EQ(record_parameters_.frames_per_buffer(), in_number_frames); |
OSStatus result = noErr; |
// Simply return if recording is not enabled. |
if (!rtc::AtomicOps::AcquireLoad(&recording_)) |
return result; |
- RTC_DCHECK_EQ(record_parameters_.frames_per_buffer(), in_number_frames); |
+ if (in_number_frames != record_parameters_.frames_per_buffer()) { |
+ // We have seen short bursts (1-2 frames) where |in_number_frames| changes. |
+ // Add a log to keep track of longer sequences if that should ever happen. |
+ LOG(LS_WARNING) << "in_number_frames (" << in_number_frames |
+ << ") != " << record_parameters_.frames_per_buffer(); |
+ } |
// Obtain the recorded audio samples by initiating a rendering cycle. |
// Since it happens on the input bus, the |io_data| parameter is a reference |
// to the preallocated audio buffer list that the audio unit renders into. |