| /* |
| * Copyright (c) 2012 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. |
| */ |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| #import <AVFoundation/AVFoundation.h> |
| #import <Foundation/Foundation.h> |
| |
| #include "webrtc/modules/audio_device/ios/audio_device_ios.h" |
| #include "webrtc/modules/utility/interface/helpers_ios.h" |
| |
| #include "webrtc/base/checks.h" |
| #include "webrtc/base/logging.h" |
| #include "webrtc/system_wrappers/interface/trace.h" |
| |
| namespace webrtc { |
| |
| #define LOGI() LOG(LS_INFO) << "AudioDeviceIOS::" |
| |
| using ios::CheckAndLogError; |
| |
| #if !defined(NDEBUG) |
| static void LogDeviceInfo() { |
| LOG(LS_INFO) << "LogDeviceInfo"; |
| @autoreleasepool { |
| LOG(LS_INFO) << " system name: " << ios::GetSystemName(); |
| LOG(LS_INFO) << " system version: " << ios::GetSystemVersion(); |
| LOG(LS_INFO) << " device type: " << ios::GetDeviceType(); |
| LOG(LS_INFO) << " device name: " << ios::GetDeviceName(); |
| } |
| } |
| #endif |
| |
| static void ActivateAudioSession(AVAudioSession* session, bool activate) { |
| LOG(LS_INFO) << "ActivateAudioSession(" << activate << ")"; |
| @autoreleasepool { |
| NSError* error = nil; |
| BOOL success = NO; |
| if (!activate) { |
| // Deactivate the audio session. |
| success = [session setActive:NO error:&error]; |
| DCHECK(CheckAndLogError(success, error)); |
| return; |
| } |
| // Activate an audio session and set category and mode. Only make changes |
| // if needed since setting them to the value they already have will clear |
| // transient properties (such as PortOverride) that some other component |
| // have set up. |
| if (session.category != AVAudioSessionCategoryPlayAndRecord) { |
| error = nil; |
| success = [session setCategory:AVAudioSessionCategoryPlayAndRecord |
| error:&error]; |
| DCHECK(CheckAndLogError(success, error)); |
| } |
| if (session.mode != AVAudioSessionModeVoiceChat) { |
| error = nil; |
| success = [session setMode:AVAudioSessionModeVoiceChat error:&error]; |
| DCHECK(CheckAndLogError(success, error)); |
| } |
| error = nil; |
| success = [session setActive:YES error:&error]; |
| DCHECK(CheckAndLogError(success, error)); |
| // Ensure that category and mode are actually activated. |
| DCHECK( |
| [session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]); |
| DCHECK([session.mode isEqualToString:AVAudioSessionModeVoiceChat]); |
| } |
| } |
| |
| // Query hardware characteristics, such as input and output latency, input and |
| // output channel count, hardware sample rate, hardware volume setting, and |
| // whether audio input is available. To obtain meaningful values for hardware |
| // characteristics,the audio session must be initialized and active before we |
| // query the values. |
| // TODO(henrika): Note that these characteristics can change at runtime. For |
| // instance, input sample rate may change when a user plugs in a headset. |
| static void GetHardwareAudioParameters(AudioParameters* playout_parameters, |
| AudioParameters* record_parameters) { |
| LOG(LS_INFO) << "GetHardwareAudioParameters"; |
| @autoreleasepool { |
| // Implicit initialization happens when we obtain a reference to the |
| // AVAudioSession object. |
| AVAudioSession* session = [AVAudioSession sharedInstance]; |
| // Always get values when the audio session is active. |
| ActivateAudioSession(session, true); |
| CHECK(session.isInputAvailable) << "No input path is available!"; |
| // Get current hardware parameters. |
| double sample_rate = (double)session.sampleRate; |
| double io_buffer_duration = (double)session.IOBufferDuration; |
| int output_channels = (int)session.outputNumberOfChannels; |
| int input_channels = (int)session.inputNumberOfChannels; |
| int frames_per_buffer = |
| static_cast<int>(sample_rate * io_buffer_duration + 0.5); |
| // Copy hardware parameters to output parameters. |
| playout_parameters->reset(sample_rate, output_channels, frames_per_buffer); |
| record_parameters->reset(sample_rate, input_channels, frames_per_buffer); |
| // Add logging for debugging purposes. |
| LOG(LS_INFO) << " sample rate: " << sample_rate; |
| LOG(LS_INFO) << " IO buffer duration: " << io_buffer_duration; |
| LOG(LS_INFO) << " frames_per_buffer: " << frames_per_buffer; |
| LOG(LS_INFO) << " output channels: " << output_channels; |
| LOG(LS_INFO) << " input channels: " << input_channels; |
| LOG(LS_INFO) << " output latency: " << (double)session.outputLatency; |
| LOG(LS_INFO) << " input latency: " << (double)session.inputLatency; |
| // Don't keep the audio session active. Instead, deactivate when needed. |
| ActivateAudioSession(session, false); |
| // TODO(henrika): to be extra safe, we can do more here. E.g., set |
| // preferred values for sample rate, channels etc., re-activate an audio |
| // session and verify the actual values again. Then we know for sure that |
| // the current values will in fact be correct. Or, we can skip all this |
| // and check setting when audio is started. Probably better. |
| } |
| } |
| |
| AudioDeviceIOS::AudioDeviceIOS() |
| : audio_device_buffer_(nullptr), |
| _critSect(*CriticalSectionWrapper::CreateCriticalSection()), |
| _auVoiceProcessing(nullptr), |
| _audioInterruptionObserver(nullptr), |
| _initialized(false), |
| _isShutDown(false), |
| _recording(false), |
| _playing(false), |
| _recIsInitialized(false), |
| _playIsInitialized(false), |
| _adbSampFreq(0), |
| _recordingDelay(0), |
| _playoutDelay(0), |
| _playoutDelayMeasurementCounter(9999), |
| _recordingDelayHWAndOS(0), |
| _recordingDelayMeasurementCounter(9999), |
| _playoutBufferUsed(0), |
| _recordingCurrentSeq(0), |
| _recordingBufferTotalSize(0) { |
| LOGI() << "ctor" << ios::GetCurrentThreadDescription(); |
| memset(_playoutBuffer, 0, sizeof(_playoutBuffer)); |
| memset(_recordingBuffer, 0, sizeof(_recordingBuffer)); |
| memset(_recordingLength, 0, sizeof(_recordingLength)); |
| memset(_recordingSeqNumber, 0, sizeof(_recordingSeqNumber)); |
| } |
| |
| AudioDeviceIOS::~AudioDeviceIOS() { |
| LOGI() << "~dtor"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| Terminate(); |
| delete &_critSect; |
| } |
| |
| void AudioDeviceIOS::AttachAudioBuffer(AudioDeviceBuffer* audioBuffer) { |
| LOGI() << "AttachAudioBuffer"; |
| DCHECK(audioBuffer); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| audio_device_buffer_ = audioBuffer; |
| } |
| |
| int32_t AudioDeviceIOS::Init() { |
| LOGI() << "Init"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| if (_initialized) { |
| return 0; |
| } |
| #if !defined(NDEBUG) |
| LogDeviceInfo(); |
| #endif |
| // Query hardware audio parameters and cache the results. These parameters |
| // will be used as preferred values later when streaming starts. |
| // Note that I override these "optimal" value below since I don't want to |
| // modify the existing behavior yet. |
| GetHardwareAudioParameters(&playout_parameters_, &record_parameters_); |
| // TODO(henrika): these parameters are currently hard coded to match the |
| // existing implementation where we always use 16kHz as preferred sample |
| // rate and mono only. Goal is to improve this scheme and make it more |
| // flexible. In addition, a better native buffer size shall be derived. |
| // Using 10ms as default here (only used by unit test so far). |
| // We should also implemented observers for notification of any change in |
| // these parameters. |
| playout_parameters_.reset(16000, 1, 160); |
| record_parameters_.reset(16000, 1, 160); |
| |
| // AttachAudioBuffer() is called at construction by the main class but check |
| // just in case. |
| DCHECK(audio_device_buffer_) << "AttachAudioBuffer must be called first"; |
| // Inform the audio device buffer (ADB) about the new audio format. |
| // TODO(henrika): try to improve this section. |
| audio_device_buffer_->SetPlayoutSampleRate(playout_parameters_.sample_rate()); |
| audio_device_buffer_->SetPlayoutChannels(playout_parameters_.channels()); |
| audio_device_buffer_->SetRecordingSampleRate( |
| record_parameters_.sample_rate()); |
| audio_device_buffer_->SetRecordingChannels(record_parameters_.channels()); |
| |
| DCHECK(!_captureWorkerThread); |
| // Create and start the capture thread. |
| // TODO(henrika): do we need this thread? |
| _isShutDown = false; |
| _captureWorkerThread = |
| ThreadWrapper::CreateThread(RunCapture, this, "CaptureWorkerThread"); |
| if (!_captureWorkerThread->Start()) { |
| LOG_F(LS_ERROR) << "Failed to start CaptureWorkerThread!"; |
| return -1; |
| } |
| _captureWorkerThread->SetPriority(kRealtimePriority); |
| _initialized = true; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::Terminate() { |
| LOGI() << "Terminate"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| if (!_initialized) { |
| return 0; |
| } |
| // Stop the capture thread. |
| if (_captureWorkerThread) { |
| if (!_captureWorkerThread->Stop()) { |
| LOG_F(LS_ERROR) << "Failed to stop CaptureWorkerThread!"; |
| return -1; |
| } |
| _captureWorkerThread.reset(); |
| } |
| ShutdownPlayOrRecord(); |
| _isShutDown = true; |
| _initialized = false; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::InitPlayout() { |
| LOGI() << "InitPlayout"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(_initialized); |
| DCHECK(!_playIsInitialized); |
| DCHECK(!_playing); |
| if (!_recIsInitialized) { |
| if (InitPlayOrRecord() == -1) { |
| LOG_F(LS_ERROR) << "InitPlayOrRecord failed!"; |
| return -1; |
| } |
| } |
| _playIsInitialized = true; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::InitRecording() { |
| LOGI() << "InitPlayout"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(_initialized); |
| DCHECK(!_recIsInitialized); |
| DCHECK(!_recording); |
| if (!_playIsInitialized) { |
| if (InitPlayOrRecord() == -1) { |
| LOG_F(LS_ERROR) << "InitPlayOrRecord failed!"; |
| return -1; |
| } |
| } |
| _recIsInitialized = true; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::StartPlayout() { |
| LOGI() << "StartPlayout"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(_playIsInitialized); |
| DCHECK(!_playing); |
| |
| CriticalSectionScoped lock(&_critSect); |
| |
| memset(_playoutBuffer, 0, sizeof(_playoutBuffer)); |
| _playoutBufferUsed = 0; |
| _playoutDelay = 0; |
| // Make sure first call to update delay function will update delay |
| _playoutDelayMeasurementCounter = 9999; |
| |
| if (!_recording) { |
| OSStatus result = AudioOutputUnitStart(_auVoiceProcessing); |
| if (result != noErr) { |
| LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result; |
| return -1; |
| } |
| } |
| _playing = true; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::StopPlayout() { |
| LOGI() << "StopPlayout"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| if (!_playIsInitialized || !_playing) { |
| return 0; |
| } |
| |
| CriticalSectionScoped lock(&_critSect); |
| |
| if (!_recording) { |
| // Both playout and recording has stopped, shutdown the device. |
| ShutdownPlayOrRecord(); |
| } |
| _playIsInitialized = false; |
| _playing = false; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::StartRecording() { |
| LOGI() << "StartRecording"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(_recIsInitialized); |
| DCHECK(!_recording); |
| |
| CriticalSectionScoped lock(&_critSect); |
| |
| memset(_recordingBuffer, 0, sizeof(_recordingBuffer)); |
| memset(_recordingLength, 0, sizeof(_recordingLength)); |
| memset(_recordingSeqNumber, 0, sizeof(_recordingSeqNumber)); |
| |
| _recordingCurrentSeq = 0; |
| _recordingBufferTotalSize = 0; |
| _recordingDelay = 0; |
| _recordingDelayHWAndOS = 0; |
| // Make sure first call to update delay function will update delay |
| _recordingDelayMeasurementCounter = 9999; |
| |
| if (!_playing) { |
| OSStatus result = AudioOutputUnitStart(_auVoiceProcessing); |
| if (result != noErr) { |
| LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result; |
| return -1; |
| } |
| } |
| _recording = true; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::StopRecording() { |
| LOGI() << "StopRecording"; |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| if (!_recIsInitialized || !_recording) { |
| return 0; |
| } |
| |
| CriticalSectionScoped lock(&_critSect); |
| |
| if (!_playing) { |
| // Both playout and recording has stopped, shutdown the device. |
| ShutdownPlayOrRecord(); |
| } |
| _recIsInitialized = false; |
| _recording = false; |
| return 0; |
| } |
| |
| // Change the default receiver playout route to speaker. |
| int32_t AudioDeviceIOS::SetLoudspeakerStatus(bool enable) { |
| LOGI() << "SetLoudspeakerStatus(" << enable << ")"; |
| |
| AVAudioSession* session = [AVAudioSession sharedInstance]; |
| NSString* category = session.category; |
| AVAudioSessionCategoryOptions options = session.categoryOptions; |
| // Respect old category options if category is |
| // AVAudioSessionCategoryPlayAndRecord. Otherwise reset it since old options |
| // might not be valid for this category. |
| if ([category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { |
| if (enable) { |
| options |= AVAudioSessionCategoryOptionDefaultToSpeaker; |
| } else { |
| options &= ~AVAudioSessionCategoryOptionDefaultToSpeaker; |
| } |
| } else { |
| options = AVAudioSessionCategoryOptionDefaultToSpeaker; |
| } |
| NSError* error = nil; |
| BOOL success = [session setCategory:AVAudioSessionCategoryPlayAndRecord |
| withOptions:options |
| error:&error]; |
| ios::CheckAndLogError(success, error); |
| return (error == nil) ? 0 : -1; |
| } |
| |
| int32_t AudioDeviceIOS::GetLoudspeakerStatus(bool& enabled) const { |
| LOGI() << "GetLoudspeakerStatus"; |
| AVAudioSession* session = [AVAudioSession sharedInstance]; |
| AVAudioSessionCategoryOptions options = session.categoryOptions; |
| enabled = options & AVAudioSessionCategoryOptionDefaultToSpeaker; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::PlayoutDelay(uint16_t& delayMS) const { |
| delayMS = _playoutDelay; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::RecordingDelay(uint16_t& delayMS) const { |
| delayMS = _recordingDelay; |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::PlayoutBuffer(AudioDeviceModule::BufferType& type, |
| uint16_t& sizeMS) const { |
| type = AudioDeviceModule::kAdaptiveBufferSize; |
| sizeMS = _playoutDelay; |
| return 0; |
| } |
| |
| int AudioDeviceIOS::GetPlayoutAudioParameters(AudioParameters* params) const { |
| CHECK(playout_parameters_.is_valid()); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| *params = playout_parameters_; |
| return 0; |
| } |
| |
| int AudioDeviceIOS::GetRecordAudioParameters(AudioParameters* params) const { |
| CHECK(record_parameters_.is_valid()); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| *params = record_parameters_; |
| return 0; |
| } |
| |
| // ============================================================================ |
| // Private Methods |
| // ============================================================================ |
| |
| int32_t AudioDeviceIOS::InitPlayOrRecord() { |
| LOGI() << "AudioDeviceIOS::InitPlayOrRecord"; |
| DCHECK(!_auVoiceProcessing); |
| |
| OSStatus result = -1; |
| |
| // Create Voice Processing Audio Unit |
| AudioComponentDescription desc; |
| AudioComponent comp; |
| |
| desc.componentType = kAudioUnitType_Output; |
| desc.componentSubType = kAudioUnitSubType_VoiceProcessingIO; |
| desc.componentManufacturer = kAudioUnitManufacturer_Apple; |
| desc.componentFlags = 0; |
| desc.componentFlagsMask = 0; |
| |
| comp = AudioComponentFindNext(nullptr, &desc); |
| if (nullptr == comp) { |
| LOG_F(LS_ERROR) << "Could not find audio component for Audio Unit"; |
| return -1; |
| } |
| |
| result = AudioComponentInstanceNew(comp, &_auVoiceProcessing); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to create Audio Unit instance: " << result; |
| return -1; |
| } |
| |
| // TODO(henrika): I think we should set the preferred channel configuration |
| // in both directions as well to be safe. |
| |
| // Set preferred hardware sample rate to 16 kHz. |
| // TODO(henrika): improve this selection of sample rate. Why do we currently |
| // use a hard coded value? How can we fail and still continue? |
| NSError* error = nil; |
| AVAudioSession* session = [AVAudioSession sharedInstance]; |
| Float64 preferredSampleRate(playout_parameters_.sample_rate()); |
| [session setPreferredSampleRate:preferredSampleRate error:&error]; |
| if (error != nil) { |
| const char* errorString = [[error localizedDescription] UTF8String]; |
| LOG_F(LS_ERROR) << "setPreferredSampleRate failed: " << errorString; |
| } |
| |
| // TODO(henrika): we can reduce latency by setting the IOBufferDuration |
| // here. Default size for 16kHz is 0.016 sec or 16 msec on an iPhone 6. |
| |
| // Activate the audio session. |
| ActivateAudioSession(session, true); |
| |
| UInt32 enableIO = 1; |
| result = AudioUnitSetProperty(_auVoiceProcessing, |
| kAudioOutputUnitProperty_EnableIO, |
| kAudioUnitScope_Input, |
| 1, // input bus |
| &enableIO, sizeof(enableIO)); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to enable IO on input: " << result; |
| } |
| |
| result = AudioUnitSetProperty(_auVoiceProcessing, |
| kAudioOutputUnitProperty_EnableIO, |
| kAudioUnitScope_Output, |
| 0, // output bus |
| &enableIO, sizeof(enableIO)); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to enable IO on output: " << result; |
| } |
| |
| // Disable AU buffer allocation for the recorder, we allocate our own. |
| // TODO(henrika): understand this part better. |
| UInt32 flag = 0; |
| result = AudioUnitSetProperty(_auVoiceProcessing, |
| kAudioUnitProperty_ShouldAllocateBuffer, |
| kAudioUnitScope_Output, 1, &flag, sizeof(flag)); |
| if (0 != result) { |
| LOG_F(LS_WARNING) << "Failed to disable AU buffer allocation: " << result; |
| // Should work anyway |
| } |
| |
| // Set recording callback. |
| AURenderCallbackStruct auCbS; |
| memset(&auCbS, 0, sizeof(auCbS)); |
| auCbS.inputProc = RecordProcess; |
| auCbS.inputProcRefCon = this; |
| result = AudioUnitSetProperty( |
| _auVoiceProcessing, kAudioOutputUnitProperty_SetInputCallback, |
| kAudioUnitScope_Global, 1, &auCbS, sizeof(auCbS)); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to set AU record callback: " << result; |
| } |
| |
| // Set playout callback. |
| memset(&auCbS, 0, sizeof(auCbS)); |
| auCbS.inputProc = PlayoutProcess; |
| auCbS.inputProcRefCon = this; |
| result = AudioUnitSetProperty( |
| _auVoiceProcessing, kAudioUnitProperty_SetRenderCallback, |
| kAudioUnitScope_Global, 0, &auCbS, sizeof(auCbS)); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to set AU output callback: " << result; |
| } |
| |
| // Get stream format for out/0 |
| AudioStreamBasicDescription playoutDesc; |
| UInt32 size = sizeof(playoutDesc); |
| result = |
| AudioUnitGetProperty(_auVoiceProcessing, kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Output, 0, &playoutDesc, &size); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to get AU output stream format: " << result; |
| } |
| |
| playoutDesc.mSampleRate = preferredSampleRate; |
| LOG(LS_INFO) << "Audio Unit playout opened in sampling rate: " |
| << playoutDesc.mSampleRate; |
| |
| // Store the sampling frequency to use towards the Audio Device Buffer |
| // todo: Add 48 kHz (increase buffer sizes). Other fs? |
| // TODO(henrika): Figure out if we really need this complex handling. |
| if ((playoutDesc.mSampleRate > 44090.0) && |
| (playoutDesc.mSampleRate < 44110.0)) { |
| _adbSampFreq = 44100; |
| } else if ((playoutDesc.mSampleRate > 15990.0) && |
| (playoutDesc.mSampleRate < 16010.0)) { |
| _adbSampFreq = 16000; |
| } else if ((playoutDesc.mSampleRate > 7990.0) && |
| (playoutDesc.mSampleRate < 8010.0)) { |
| _adbSampFreq = 8000; |
| } else { |
| _adbSampFreq = 0; |
| FATAL() << "Invalid sample rate"; |
| } |
| |
| // Set the audio device buffer sampling rates (use same for play and record). |
| // TODO(henrika): this is not a good place to set these things up. |
| DCHECK(audio_device_buffer_); |
| DCHECK_EQ(_adbSampFreq, playout_parameters_.sample_rate()); |
| audio_device_buffer_->SetRecordingSampleRate(_adbSampFreq); |
| audio_device_buffer_->SetPlayoutSampleRate(_adbSampFreq); |
| |
| // Set stream format for out/0. |
| playoutDesc.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | |
| kLinearPCMFormatFlagIsPacked | |
| kLinearPCMFormatFlagIsNonInterleaved; |
| playoutDesc.mBytesPerPacket = 2; |
| playoutDesc.mFramesPerPacket = 1; |
| playoutDesc.mBytesPerFrame = 2; |
| playoutDesc.mChannelsPerFrame = 1; |
| playoutDesc.mBitsPerChannel = 16; |
| result = |
| AudioUnitSetProperty(_auVoiceProcessing, kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Input, 0, &playoutDesc, size); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to set AU stream format for out/0"; |
| } |
| |
| // Get stream format for in/1. |
| AudioStreamBasicDescription recordingDesc; |
| size = sizeof(recordingDesc); |
| result = |
| AudioUnitGetProperty(_auVoiceProcessing, kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Input, 1, &recordingDesc, &size); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to get AU stream format for in/1"; |
| } |
| |
| recordingDesc.mSampleRate = preferredSampleRate; |
| LOG(LS_INFO) << "Audio Unit recording opened in sampling rate: " |
| << recordingDesc.mSampleRate; |
| |
| // Set stream format for out/1 (use same sampling frequency as for in/1). |
| recordingDesc.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | |
| kLinearPCMFormatFlagIsPacked | |
| kLinearPCMFormatFlagIsNonInterleaved; |
| recordingDesc.mBytesPerPacket = 2; |
| recordingDesc.mFramesPerPacket = 1; |
| recordingDesc.mBytesPerFrame = 2; |
| recordingDesc.mChannelsPerFrame = 1; |
| recordingDesc.mBitsPerChannel = 16; |
| result = |
| AudioUnitSetProperty(_auVoiceProcessing, kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Output, 1, &recordingDesc, size); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "Failed to set AU stream format for out/1"; |
| } |
| |
| // Initialize here already to be able to get/set stream properties. |
| result = AudioUnitInitialize(_auVoiceProcessing); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "AudioUnitInitialize failed: " << result; |
| } |
| |
| // Get hardware sample rate for logging (see if we get what we asked for). |
| // TODO(henrika): what if we don't get what we ask for? |
| double sampleRate = session.sampleRate; |
| LOG(LS_INFO) << "Current HW sample rate is: " << sampleRate |
| << ", ADB sample rate is: " << _adbSampFreq; |
| LOG(LS_INFO) << "Current HW IO buffer size is: " << |
| [session IOBufferDuration]; |
| |
| // 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(_auVoiceProcessing); |
| AudioOutputUnitStart(_auVoiceProcessing); |
| 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. |
| _audioInterruptionObserver = (__bridge_retained void*)observer; |
| |
| // Deactivate the audio session. |
| ActivateAudioSession(session, false); |
| |
| return 0; |
| } |
| |
| int32_t AudioDeviceIOS::ShutdownPlayOrRecord() { |
| LOGI() << "ShutdownPlayOrRecord"; |
| |
| if (_audioInterruptionObserver != 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)_audioInterruptionObserver; |
| [center removeObserver:observer]; |
| _audioInterruptionObserver = nullptr; |
| } |
| |
| // Close and delete AU. |
| OSStatus result = -1; |
| if (nullptr != _auVoiceProcessing) { |
| result = AudioOutputUnitStop(_auVoiceProcessing); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "AudioOutputUnitStop failed: " << result; |
| } |
| result = AudioComponentInstanceDispose(_auVoiceProcessing); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "AudioComponentInstanceDispose failed: " << result; |
| } |
| _auVoiceProcessing = nullptr; |
| } |
| |
| return 0; |
| } |
| |
| // ============================================================================ |
| // Thread Methods |
| // ============================================================================ |
| |
| OSStatus AudioDeviceIOS::RecordProcess( |
| void* inRefCon, |
| AudioUnitRenderActionFlags* ioActionFlags, |
| const AudioTimeStamp* inTimeStamp, |
| UInt32 inBusNumber, |
| UInt32 inNumberFrames, |
| AudioBufferList* ioData) { |
| AudioDeviceIOS* ptrThis = static_cast<AudioDeviceIOS*>(inRefCon); |
| return ptrThis->RecordProcessImpl(ioActionFlags, inTimeStamp, inBusNumber, |
| inNumberFrames); |
| } |
| |
| OSStatus AudioDeviceIOS::RecordProcessImpl( |
| AudioUnitRenderActionFlags* ioActionFlags, |
| const AudioTimeStamp* inTimeStamp, |
| uint32_t inBusNumber, |
| uint32_t inNumberFrames) { |
| // Setup some basic stuff |
| // Use temp buffer not to lock up recording buffer more than necessary |
| // todo: Make dataTmp a member variable with static size that holds |
| // max possible frames? |
| int16_t* dataTmp = new int16_t[inNumberFrames]; |
| memset(dataTmp, 0, 2 * inNumberFrames); |
| |
| AudioBufferList abList; |
| abList.mNumberBuffers = 1; |
| abList.mBuffers[0].mData = dataTmp; |
| abList.mBuffers[0].mDataByteSize = 2 * inNumberFrames; // 2 bytes/sample |
| abList.mBuffers[0].mNumberChannels = 1; |
| |
| // Get data from mic |
| OSStatus res = AudioUnitRender(_auVoiceProcessing, ioActionFlags, inTimeStamp, |
| inBusNumber, inNumberFrames, &abList); |
| if (res != 0) { |
| // TODO(henrika): improve error handling. |
| delete[] dataTmp; |
| return 0; |
| } |
| |
| if (_recording) { |
| // Insert all data in temp buffer into recording buffers |
| // There is zero or one buffer partially full at any given time, |
| // all others are full or empty |
| // Full means filled with noSamp10ms samples. |
| |
| const unsigned int noSamp10ms = _adbSampFreq / 100; |
| unsigned int dataPos = 0; |
| uint16_t bufPos = 0; |
| int16_t insertPos = -1; |
| unsigned int nCopy = 0; // Number of samples to copy |
| |
| while (dataPos < inNumberFrames) { |
| // Loop over all recording buffers or |
| // until we find the partially full buffer |
| // First choice is to insert into partially full buffer, |
| // second choice is to insert into empty buffer |
| bufPos = 0; |
| insertPos = -1; |
| nCopy = 0; |
| while (bufPos < N_REC_BUFFERS) { |
| if ((_recordingLength[bufPos] > 0) && |
| (_recordingLength[bufPos] < noSamp10ms)) { |
| // Found the partially full buffer |
| insertPos = static_cast<int16_t>(bufPos); |
| // Don't need to search more, quit loop |
| bufPos = N_REC_BUFFERS; |
| } else if ((-1 == insertPos) && (0 == _recordingLength[bufPos])) { |
| // Found an empty buffer |
| insertPos = static_cast<int16_t>(bufPos); |
| } |
| ++bufPos; |
| } |
| |
| // Insert data into buffer |
| if (insertPos > -1) { |
| // We found a non-full buffer, copy data to it |
| unsigned int dataToCopy = inNumberFrames - dataPos; |
| unsigned int currentRecLen = _recordingLength[insertPos]; |
| unsigned int roomInBuffer = noSamp10ms - currentRecLen; |
| nCopy = (dataToCopy < roomInBuffer ? dataToCopy : roomInBuffer); |
| |
| memcpy(&_recordingBuffer[insertPos][currentRecLen], &dataTmp[dataPos], |
| nCopy * sizeof(int16_t)); |
| if (0 == currentRecLen) { |
| _recordingSeqNumber[insertPos] = _recordingCurrentSeq; |
| ++_recordingCurrentSeq; |
| } |
| _recordingBufferTotalSize += nCopy; |
| // Has to be done last to avoid interrupt problems between threads. |
| _recordingLength[insertPos] += nCopy; |
| dataPos += nCopy; |
| } else { |
| // Didn't find a non-full buffer |
| // TODO(henrika): improve error handling |
| dataPos = inNumberFrames; // Don't try to insert more |
| } |
| } |
| } |
| delete[] dataTmp; |
| return 0; |
| } |
| |
| OSStatus AudioDeviceIOS::PlayoutProcess( |
| void* inRefCon, |
| AudioUnitRenderActionFlags* ioActionFlags, |
| const AudioTimeStamp* inTimeStamp, |
| UInt32 inBusNumber, |
| UInt32 inNumberFrames, |
| AudioBufferList* ioData) { |
| AudioDeviceIOS* ptrThis = static_cast<AudioDeviceIOS*>(inRefCon); |
| return ptrThis->PlayoutProcessImpl(inNumberFrames, ioData); |
| } |
| |
| OSStatus AudioDeviceIOS::PlayoutProcessImpl(uint32_t inNumberFrames, |
| AudioBufferList* ioData) { |
| int16_t* data = static_cast<int16_t*>(ioData->mBuffers[0].mData); |
| unsigned int dataSizeBytes = ioData->mBuffers[0].mDataByteSize; |
| unsigned int dataSize = dataSizeBytes / 2; // Number of samples |
| CHECK_EQ(dataSize, inNumberFrames); |
| memset(data, 0, dataSizeBytes); // Start with empty buffer |
| |
| // Get playout data from Audio Device Buffer |
| |
| if (_playing) { |
| unsigned int noSamp10ms = _adbSampFreq / 100; |
| // todo: Member variable and allocate when samp freq is determined |
| int16_t* dataTmp = new int16_t[noSamp10ms]; |
| memset(dataTmp, 0, 2 * noSamp10ms); |
| unsigned int dataPos = 0; |
| int noSamplesOut = 0; |
| unsigned int nCopy = 0; |
| |
| // First insert data from playout buffer if any |
| if (_playoutBufferUsed > 0) { |
| nCopy = (dataSize < _playoutBufferUsed) ? dataSize : _playoutBufferUsed; |
| DCHECK_EQ(nCopy, _playoutBufferUsed); |
| memcpy(data, _playoutBuffer, 2 * nCopy); |
| dataPos = nCopy; |
| memset(_playoutBuffer, 0, sizeof(_playoutBuffer)); |
| _playoutBufferUsed = 0; |
| } |
| |
| // Now get the rest from Audio Device Buffer. |
| while (dataPos < dataSize) { |
| // Update playout delay |
| UpdatePlayoutDelay(); |
| |
| // Ask for new PCM data to be played out using the AudioDeviceBuffer |
| noSamplesOut = audio_device_buffer_->RequestPlayoutData(noSamp10ms); |
| |
| // Get data from Audio Device Buffer |
| noSamplesOut = audio_device_buffer_->GetPlayoutData( |
| reinterpret_cast<int8_t*>(dataTmp)); |
| CHECK_EQ(noSamp10ms, (unsigned int)noSamplesOut); |
| |
| // Insert as much as fits in data buffer |
| nCopy = |
| (dataSize - dataPos) > noSamp10ms ? noSamp10ms : (dataSize - dataPos); |
| memcpy(&data[dataPos], dataTmp, 2 * nCopy); |
| |
| // Save rest in playout buffer if any |
| if (nCopy < noSamp10ms) { |
| memcpy(_playoutBuffer, &dataTmp[nCopy], 2 * (noSamp10ms - nCopy)); |
| _playoutBufferUsed = noSamp10ms - nCopy; |
| } |
| |
| // Update loop/index counter, if we copied less than noSamp10ms |
| // samples we shall quit loop anyway |
| dataPos += noSamp10ms; |
| } |
| delete[] dataTmp; |
| } |
| return 0; |
| } |
| |
| // TODO(henrika): can either be removed or simplified. |
| void AudioDeviceIOS::UpdatePlayoutDelay() { |
| ++_playoutDelayMeasurementCounter; |
| |
| if (_playoutDelayMeasurementCounter >= 100) { |
| // Update HW and OS delay every second, unlikely to change |
| |
| // Since this is eventually rounded to integral ms, add 0.5ms |
| // here to get round-to-nearest-int behavior instead of |
| // truncation. |
| double totalDelaySeconds = 0.0005; |
| |
| // HW output latency |
| AVAudioSession* session = [AVAudioSession sharedInstance]; |
| double latency = session.outputLatency; |
| assert(latency >= 0); |
| totalDelaySeconds += latency; |
| |
| // HW buffer duration |
| double ioBufferDuration = session.IOBufferDuration; |
| assert(ioBufferDuration >= 0); |
| totalDelaySeconds += ioBufferDuration; |
| |
| // AU latency |
| Float64 f64(0); |
| UInt32 size = sizeof(f64); |
| OSStatus result = |
| AudioUnitGetProperty(_auVoiceProcessing, kAudioUnitProperty_Latency, |
| kAudioUnitScope_Global, 0, &f64, &size); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "AU latency error: " << result; |
| } |
| assert(f64 >= 0); |
| totalDelaySeconds += f64; |
| |
| // To ms |
| _playoutDelay = static_cast<uint32_t>(totalDelaySeconds / 1000); |
| |
| // Reset counter |
| _playoutDelayMeasurementCounter = 0; |
| } |
| |
| // todo: Add playout buffer? |
| } |
| |
| void AudioDeviceIOS::UpdateRecordingDelay() { |
| ++_recordingDelayMeasurementCounter; |
| |
| if (_recordingDelayMeasurementCounter >= 100) { |
| // Update HW and OS delay every second, unlikely to change |
| |
| // Since this is eventually rounded to integral ms, add 0.5ms |
| // here to get round-to-nearest-int behavior instead of |
| // truncation. |
| double totalDelaySeconds = 0.0005; |
| |
| // HW input latency |
| AVAudioSession* session = [AVAudioSession sharedInstance]; |
| double latency = session.inputLatency; |
| assert(latency >= 0); |
| totalDelaySeconds += latency; |
| |
| // HW buffer duration |
| double ioBufferDuration = session.IOBufferDuration; |
| assert(ioBufferDuration >= 0); |
| totalDelaySeconds += ioBufferDuration; |
| |
| // AU latency |
| Float64 f64(0); |
| UInt32 size = sizeof(f64); |
| OSStatus result = |
| AudioUnitGetProperty(_auVoiceProcessing, kAudioUnitProperty_Latency, |
| kAudioUnitScope_Global, 0, &f64, &size); |
| if (0 != result) { |
| LOG_F(LS_ERROR) << "AU latency error: " << result; |
| } |
| assert(f64 >= 0); |
| totalDelaySeconds += f64; |
| |
| // To ms |
| _recordingDelayHWAndOS = static_cast<uint32_t>(totalDelaySeconds / 1000); |
| |
| // Reset counter |
| _recordingDelayMeasurementCounter = 0; |
| } |
| |
| _recordingDelay = _recordingDelayHWAndOS; |
| |
| // ADB recording buffer size, update every time |
| // Don't count the one next 10 ms to be sent, then convert samples => ms |
| const uint32_t noSamp10ms = _adbSampFreq / 100; |
| if (_recordingBufferTotalSize > noSamp10ms) { |
| _recordingDelay += |
| (_recordingBufferTotalSize - noSamp10ms) / (_adbSampFreq / 1000); |
| } |
| } |
| |
| bool AudioDeviceIOS::RunCapture(void* ptrThis) { |
| return static_cast<AudioDeviceIOS*>(ptrThis)->CaptureWorkerThread(); |
| } |
| |
| bool AudioDeviceIOS::CaptureWorkerThread() { |
| if (_recording) { |
| int bufPos = 0; |
| unsigned int lowestSeq = 0; |
| int lowestSeqBufPos = 0; |
| bool foundBuf = true; |
| const unsigned int noSamp10ms = _adbSampFreq / 100; |
| |
| while (foundBuf) { |
| // Check if we have any buffer with data to insert |
| // into the Audio Device Buffer, |
| // and find the one with the lowest seq number |
| foundBuf = false; |
| for (bufPos = 0; bufPos < N_REC_BUFFERS; ++bufPos) { |
| if (noSamp10ms == _recordingLength[bufPos]) { |
| if (!foundBuf) { |
| lowestSeq = _recordingSeqNumber[bufPos]; |
| lowestSeqBufPos = bufPos; |
| foundBuf = true; |
| } else if (_recordingSeqNumber[bufPos] < lowestSeq) { |
| lowestSeq = _recordingSeqNumber[bufPos]; |
| lowestSeqBufPos = bufPos; |
| } |
| } |
| } |
| |
| // Insert data into the Audio Device Buffer if found any |
| if (foundBuf) { |
| // Update recording delay |
| UpdateRecordingDelay(); |
| |
| // Set the recorded buffer |
| audio_device_buffer_->SetRecordedBuffer( |
| reinterpret_cast<int8_t*>(_recordingBuffer[lowestSeqBufPos]), |
| _recordingLength[lowestSeqBufPos]); |
| |
| // Don't need to set the current mic level in ADB since we only |
| // support digital AGC, |
| // and besides we cannot get or set the IOS mic level anyway. |
| |
| // Set VQE info, use clockdrift == 0 |
| audio_device_buffer_->SetVQEData(_playoutDelay, _recordingDelay, 0); |
| |
| // Deliver recorded samples at specified sample rate, mic level |
| // etc. to the observer using callback |
| audio_device_buffer_->DeliverRecordedData(); |
| |
| // Make buffer available |
| _recordingSeqNumber[lowestSeqBufPos] = 0; |
| _recordingBufferTotalSize -= _recordingLength[lowestSeqBufPos]; |
| // Must be done last to avoid interrupt problems between threads |
| _recordingLength[lowestSeqBufPos] = 0; |
| } |
| } |
| } |
| |
| { |
| // Normal case |
| // Sleep thread (5ms) to let other threads get to work |
| // todo: Is 5 ms optimal? Sleep shorter if inserted into the Audio |
| // Device Buffer? |
| timespec t; |
| t.tv_sec = 0; |
| t.tv_nsec = 5 * 1000 * 1000; |
| nanosleep(&t, nullptr); |
| } |
| return true; |
| } |
| |
| } // namespace webrtc |