/*
 *  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/base/atomicops.h"
#include "webrtc/base/checks.h"
#include "webrtc/base/logging.h"
#include "webrtc/modules/audio_device/fine_audio_buffer.h"
#include "webrtc/modules/utility/include/helpers_ios.h"

namespace webrtc {

#define LOGI() LOG(LS_INFO) << "AudioDeviceIOS::"

#define LOG_AND_RETURN_IF_ERROR(error, message) \
  do {                                          \
    OSStatus err = error;                       \
    if (err) {                                  \
      LOG(LS_ERROR) << message << ": " << err;  \
      return false;                             \
    }                                           \
  } 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
// 8000Hz as native sample rate.
const double kPreferredSampleRate = 48000.0;
// Use a hardware I/O buffer size (unit is in seconds) that matches the 10ms
// size used by WebRTC. The exact actual size will differ between devices.
// Example: using 48kHz on iPhone 6 results in a native buffer size of
// ~10.6667ms or 512 audio frames per buffer. The FineAudioBuffer instance will
// take care of any buffering required to convert between native buffers and
// buffers used by WebRTC. It is beneficial for the performance if the native
// size is as close to 10ms as possible since it results in "clean" callback
// sequence without bursts of callbacks back to back.
const double kPreferredIOBufferDuration = 0.01;
// Try to use mono to save resources. Also avoids channel format conversion
// in the I/O audio unit. Initial tests have shown that it is possible to use
// mono natively for built-in microphones and for BT headsets but not for
// wired headsets. Wired headsets only support stereo as native channel format
// but it is a low cost operation to do a format conversion to mono in the
// audio unit. Hence, we will not hit a RTC_CHECK in
// VerifyAudioParametersForActiveAudioSession() for a mismatch between the
// preferred number of channels and the actual number of channels.
const int kPreferredNumberOfChannels = 1;
// Number of bytes per audio sample for 16-bit signed integer representation.
const UInt32 kBytesPerSample = 2;
// Hardcoded delay estimates based on real measurements.
// TODO(henrika): these value is not used in combination with built-in AEC.
// Can most likely be removed.
const UInt16 kFixedPlayoutDelayEstimate = 30;
const UInt16 kFixedRecordDelayEstimate = 30;

using ios::CheckAndLogError;

// 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) {
  LOG(LS_INFO) << "ActivateAudioSession(" << 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
    // audio sessions which are also nonmixable.
    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.
    error = nil;
    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;
    success = [session setPreferredIOBufferDuration:kPreferredIOBufferDuration
                                              error:&error];
    RTC_DCHECK(CheckAndLogError(success, error));

    // 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.
    // We try to use mono in both directions to save resources and format
    // conversions in the audio unit. Some devices does only support stereo;
    // e.g. wired headset on iPhone 6.
    // TODO(henrika): add support for stereo if needed.
    error = nil;
    success =
        [session setPreferredInputNumberOfChannels:kPreferredNumberOfChannels
                                             error:&error];
    RTC_DCHECK(CheckAndLogError(success, error));
    error = nil;
    success =
        [session setPreferredOutputNumberOfChannels:kPreferredNumberOfChannels
                                              error:&error];
    RTC_DCHECK(CheckAndLogError(success, error));
  }
}

#if !defined(NDEBUG)
// Helper method for printing out an AudioStreamBasicDescription structure.
static void LogABSD(AudioStreamBasicDescription absd) {
  char formatIDString[5];
  UInt32 formatID = CFSwapInt32HostToBig(absd.mFormatID);
  bcopy(&formatID, formatIDString, 4);
  formatIDString[4] = '\0';
  LOG(LS_INFO) << "LogABSD";
  LOG(LS_INFO) << " sample rate: " << absd.mSampleRate;
  LOG(LS_INFO) << " format ID: " << formatIDString;
  LOG(LS_INFO) << " format flags: " << std::hex << absd.mFormatFlags;
  LOG(LS_INFO) << " bytes per packet: " << absd.mBytesPerPacket;
  LOG(LS_INFO) << " frames per packet: " << absd.mFramesPerPacket;
  LOG(LS_INFO) << " bytes per frame: " << absd.mBytesPerFrame;
  LOG(LS_INFO) << " channels per packet: " << absd.mChannelsPerFrame;
  LOG(LS_INFO) << " bits per channel: " << absd.mBitsPerChannel;
  LOG(LS_INFO) << " reserved: " << absd.mReserved;
}

// Helper method that logs essential device information strings.
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  // !defined(NDEBUG)

AudioDeviceIOS::AudioDeviceIOS()
    : audio_device_buffer_(nullptr),
      vpio_unit_(nullptr),
      recording_(0),
      playing_(0),
      initialized_(false),
      rec_is_initialized_(false),
      play_is_initialized_(false),
      audio_interruption_observer_(nullptr) {
  LOGI() << "ctor" << ios::GetCurrentThreadDescription();
}

AudioDeviceIOS::~AudioDeviceIOS() {
  LOGI() << "~dtor";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  Terminate();
}

void AudioDeviceIOS::AttachAudioBuffer(AudioDeviceBuffer* audioBuffer) {
  LOGI() << "AttachAudioBuffer";
  RTC_DCHECK(audioBuffer);
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  audio_device_buffer_ = audioBuffer;
}

int32_t AudioDeviceIOS::Init() {
  LOGI() << "Init";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  if (initialized_) {
    return 0;
  }
#if !defined(NDEBUG)
  LogDeviceInfo();
#endif
  // Store the preferred sample rate and preferred number of channels already
  // here. They have not been set and confirmed yet since ActivateAudioSession()
  // is not called until audio is about to start. However, it makes sense to
  // store the parameters now and then verify at a later stage.
  playout_parameters_.reset(kPreferredSampleRate, kPreferredNumberOfChannels);
  record_parameters_.reset(kPreferredSampleRate, kPreferredNumberOfChannels);
  // Ensure that the audio device buffer (ADB) knows about the internal audio
  // parameters. Note that, even if we are unable to get a mono audio session,
  // we will always tell the I/O audio unit to do a channel format conversion
  // to guarantee mono on the "input side" of the audio unit.
  UpdateAudioDeviceBuffer();
  initialized_ = true;
  return 0;
}

int32_t AudioDeviceIOS::Terminate() {
  LOGI() << "Terminate";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  if (!initialized_) {
    return 0;
  }
  ShutdownPlayOrRecord();
  initialized_ = false;
  return 0;
}

int32_t AudioDeviceIOS::InitPlayout() {
  LOGI() << "InitPlayout";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  RTC_DCHECK(initialized_);
  RTC_DCHECK(!play_is_initialized_);
  RTC_DCHECK(!playing_);
  if (!rec_is_initialized_) {
    if (!InitPlayOrRecord()) {
      LOG_F(LS_ERROR) << "InitPlayOrRecord failed!";
      return -1;
    }
  }
  play_is_initialized_ = true;
  return 0;
}

int32_t AudioDeviceIOS::InitRecording() {
  LOGI() << "InitRecording";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  RTC_DCHECK(initialized_);
  RTC_DCHECK(!rec_is_initialized_);
  RTC_DCHECK(!recording_);
  if (!play_is_initialized_) {
    if (!InitPlayOrRecord()) {
      LOG_F(LS_ERROR) << "InitPlayOrRecord failed!";
      return -1;
    }
  }
  rec_is_initialized_ = true;
  return 0;
}

int32_t AudioDeviceIOS::StartPlayout() {
  LOGI() << "StartPlayout";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  RTC_DCHECK(play_is_initialized_);
  RTC_DCHECK(!playing_);
  fine_audio_buffer_->ResetPlayout();
  if (!recording_) {
    OSStatus result = AudioOutputUnitStart(vpio_unit_);
    if (result != noErr) {
      LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result;
      return -1;
    }
  }
  rtc::AtomicOps::ReleaseStore(&playing_, 1);
  return 0;
}

int32_t AudioDeviceIOS::StopPlayout() {
  LOGI() << "StopPlayout";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  if (!play_is_initialized_ || !playing_) {
    return 0;
  }
  if (!recording_) {
    ShutdownPlayOrRecord();
  }
  play_is_initialized_ = false;
  rtc::AtomicOps::ReleaseStore(&playing_, 0);
  return 0;
}

int32_t AudioDeviceIOS::StartRecording() {
  LOGI() << "StartRecording";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  RTC_DCHECK(rec_is_initialized_);
  RTC_DCHECK(!recording_);
  fine_audio_buffer_->ResetRecord();
  if (!playing_) {
    OSStatus result = AudioOutputUnitStart(vpio_unit_);
    if (result != noErr) {
      LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result;
      return -1;
    }
  }
  rtc::AtomicOps::ReleaseStore(&recording_, 1);
  return 0;
}

int32_t AudioDeviceIOS::StopRecording() {
  LOGI() << "StopRecording";
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  if (!rec_is_initialized_ || !recording_) {
    return 0;
  }
  if (!playing_) {
    ShutdownPlayOrRecord();
  }
  rec_is_initialized_ = false;
  rtc::AtomicOps::ReleaseStore(&recording_, 0);
  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 = kFixedPlayoutDelayEstimate;
  return 0;
}

int32_t AudioDeviceIOS::RecordingDelay(uint16_t& delayMS) const {
  delayMS = kFixedRecordDelayEstimate;
  return 0;
}

int AudioDeviceIOS::GetPlayoutAudioParameters(AudioParameters* params) const {
  LOGI() << "GetPlayoutAudioParameters";
  RTC_DCHECK(playout_parameters_.is_valid());
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  *params = playout_parameters_;
  return 0;
}

int AudioDeviceIOS::GetRecordAudioParameters(AudioParameters* params) const {
  LOGI() << "GetRecordAudioParameters";
  RTC_DCHECK(record_parameters_.is_valid());
  RTC_DCHECK(thread_checker_.CalledOnValidThread());
  *params = record_parameters_;
  return 0;
}

void AudioDeviceIOS::UpdateAudioDeviceBuffer() {
  LOGI() << "UpdateAudioDevicebuffer";
  // AttachAudioBuffer() is called at construction by the main class but check
  // just in case.
  RTC_DCHECK(audio_device_buffer_) << "AttachAudioBuffer must be called first";
  // Inform the audio device buffer (ADB) about the new audio format.
  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());
}

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";
  // 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. 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
  // that info to the existing audio parameters where it is converted into
  // number of audio frames.
  // Example: IO buffer size = 0.008 seconds <=> 128 audio frames at 16kHz.
  // Hence, 128 is the size we expect to see in upcoming render callbacks.
  playout_parameters_.reset(session.sampleRate, playout_parameters_.channels(),
                            session.IOBufferDuration);
  RTC_DCHECK(playout_parameters_.is_complete());
  record_parameters_.reset(session.sampleRate, record_parameters_.channels(),
                           session.IOBufferDuration);
  RTC_DCHECK(record_parameters_.is_complete());
  LOG(LS_INFO) << " frames per I/O buffer: "
               << playout_parameters_.frames_per_buffer();
  LOG(LS_INFO) << " bytes per I/O buffer: "
               << playout_parameters_.GetBytesPerBuffer();
  RTC_DCHECK_EQ(playout_parameters_.GetBytesPerBuffer(),
                record_parameters_.GetBytesPerBuffer());

  // Update the ADB parameters since the sample rate might have changed.
  UpdateAudioDeviceBuffer();

  // Create a modified audio buffer class which allows us to ask for,
  // or deliver, any number of samples (and not only multiple of 10ms) to match
  // the native audio unit buffer size.
  RTC_DCHECK(audio_device_buffer_);
  fine_audio_buffer_.reset(new FineAudioBuffer(
      audio_device_buffer_, playout_parameters_.GetBytesPerBuffer(),
      playout_parameters_.sample_rate()));

  // The extra/temporary playoutbuffer must be of this size to avoid
  // unnecessary memcpy while caching data between successive callbacks.
  const int required_playout_buffer_size =
      fine_audio_buffer_->RequiredPlayoutBufferSizeBytes();
  LOG(LS_INFO) << " required playout buffer size: "
               << required_playout_buffer_size;
  playout_audio_buffer_.reset(new SInt8[required_playout_buffer_size]);

  // Allocate AudioBuffers to be used as storage for the received audio.
  // The AudioBufferList structure works as a placeholder for the
  // AudioBuffer structure, which holds a pointer to the actual data buffer
  // in |record_audio_buffer_|. Recorded audio will be rendered into this memory
  // at each input callback when calling AudioUnitRender().
  const int data_byte_size = record_parameters_.GetBytesPerBuffer();
  record_audio_buffer_.reset(new SInt8[data_byte_size]);
  audio_record_buffer_list_.mNumberBuffers = 1;
  AudioBuffer* audio_buffer = &audio_record_buffer_list_.mBuffers[0];
  audio_buffer->mNumberChannels = record_parameters_.channels();
  audio_buffer->mDataByteSize = data_byte_size;
  audio_buffer->mData = record_audio_buffer_.get();
}

bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
  LOGI() << "SetupAndInitializeVoiceProcessingAudioUnit";
  RTC_DCHECK(!vpio_unit_);
  // Create an audio component description to identify the Voice-Processing
  // I/O audio unit.
  AudioComponentDescription vpio_unit_description;
  vpio_unit_description.componentType = kAudioUnitType_Output;
  vpio_unit_description.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
  vpio_unit_description.componentManufacturer = kAudioUnitManufacturer_Apple;
  vpio_unit_description.componentFlags = 0;
  vpio_unit_description.componentFlagsMask = 0;
  // Obtain an audio unit instance given the description.
  AudioComponent found_vpio_unit_ref =
      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");

  // 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");

  // 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");

  // Set the application formats for input and output:
  // - use same format in both directions
  // - avoid resampling in the I/O unit by using the hardware sample rate
  // - linear PCM => noncompressed audio data format with one frame per packet
  // - no need to specify interleaving since only mono is supported
  AudioStreamBasicDescription application_format = {0};
  UInt32 size = sizeof(application_format);
  RTC_DCHECK_EQ(playout_parameters_.sample_rate(),
                record_parameters_.sample_rate());
  RTC_DCHECK_EQ(1, kPreferredNumberOfChannels);
  application_format.mSampleRate = playout_parameters_.sample_rate();
  application_format.mFormatID = kAudioFormatLinearPCM;
  application_format.mFormatFlags =
      kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
  application_format.mBytesPerPacket = kBytesPerSample;
  application_format.mFramesPerPacket = 1;  // uncompressed
  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_);
#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");

  // 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");

  // 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");

  // 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");

  // 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
  // AudioUnitRender() method.
  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");

  // 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");
  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();

  // Create, setup and initialize a new Voice-Processing I/O unit.
  if (!SetupAndInitializeVoiceProcessingAudioUnit()) {
    return false;
  }
  return true;
}

bool 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_) {
    result = AudioOutputUnitStop(vpio_unit_);
    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];
  ActivateAudioSession(session, false);
  return true;
}

OSStatus AudioDeviceIOS::RecordedDataIsAvailable(
    void* in_ref_con,
    AudioUnitRenderActionFlags* io_action_flags,
    const AudioTimeStamp* in_time_stamp,
    UInt32 in_bus_number,
    UInt32 in_number_frames,
    AudioBufferList* io_data) {
  RTC_DCHECK_EQ(1u, in_bus_number);
  RTC_DCHECK(
      !io_data);  // no buffer should be allocated for input at this stage
  AudioDeviceIOS* audio_device_ios = static_cast<AudioDeviceIOS*>(in_ref_con);
  return audio_device_ios->OnRecordedDataIsAvailable(
      io_action_flags, in_time_stamp, in_bus_number, in_number_frames);
}

OSStatus AudioDeviceIOS::OnRecordedDataIsAvailable(
    AudioUnitRenderActionFlags* io_action_flags,
    const AudioTimeStamp* in_time_stamp,
    UInt32 in_bus_number,
    UInt32 in_number_frames) {
  OSStatus result = noErr;
  // Simply return if recording is not enabled.
  if (!rtc::AtomicOps::AcquireLoad(&recording_))
    return result;
  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.
  // TODO(henrika): should error handling be improved?
  AudioBufferList* io_data = &audio_record_buffer_list_;
  result = AudioUnitRender(vpio_unit_, io_action_flags, in_time_stamp,
                           in_bus_number, in_number_frames, io_data);
  if (result != noErr) {
    LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result;
    return result;
  }
  // Get a pointer to the recorded audio and send it to the WebRTC ADB.
  // Use the FineAudioBuffer instance to convert between native buffer size
  // and the 10ms buffer size used by WebRTC.
  const UInt32 data_size_in_bytes = io_data->mBuffers[0].mDataByteSize;
  RTC_CHECK_EQ(data_size_in_bytes / kBytesPerSample, in_number_frames);
  SInt8* data = static_cast<SInt8*>(io_data->mBuffers[0].mData);
  fine_audio_buffer_->DeliverRecordedData(data, data_size_in_bytes,
                                          kFixedPlayoutDelayEstimate,
                                          kFixedRecordDelayEstimate);
  return noErr;
}

OSStatus AudioDeviceIOS::GetPlayoutData(
    void* in_ref_con,
    AudioUnitRenderActionFlags* io_action_flags,
    const AudioTimeStamp* in_time_stamp,
    UInt32 in_bus_number,
    UInt32 in_number_frames,
    AudioBufferList* io_data) {
  RTC_DCHECK_EQ(0u, in_bus_number);
  RTC_DCHECK(io_data);
  AudioDeviceIOS* audio_device_ios = static_cast<AudioDeviceIOS*>(in_ref_con);
  return audio_device_ios->OnGetPlayoutData(io_action_flags, in_number_frames,
                                            io_data);
}

OSStatus AudioDeviceIOS::OnGetPlayoutData(
    AudioUnitRenderActionFlags* io_action_flags,
    UInt32 in_number_frames,
    AudioBufferList* io_data) {
  // Verify 16-bit, noninterleaved mono PCM signal format.
  RTC_DCHECK_EQ(1u, io_data->mNumberBuffers);
  RTC_DCHECK_EQ(1u, io_data->mBuffers[0].mNumberChannels);
  // Get pointer to internal audio buffer to which new audio data shall be
  // written.
  const UInt32 dataSizeInBytes = io_data->mBuffers[0].mDataByteSize;
  RTC_CHECK_EQ(dataSizeInBytes / kBytesPerSample, in_number_frames);
  SInt8* destination = static_cast<SInt8*>(io_data->mBuffers[0].mData);
  // Produce silence and give audio unit a hint about it if playout is not
  // activated.
  if (!rtc::AtomicOps::AcquireLoad(&playing_)) {
    *io_action_flags |= kAudioUnitRenderAction_OutputIsSilence;
    memset(destination, 0, dataSizeInBytes);
    return noErr;
  }
  // Read decoded 16-bit PCM samples from WebRTC (using a size that matches
  // the native I/O audio unit) to a preallocated intermediate buffer and
  // copy the result to the audio buffer in the |io_data| destination.
  SInt8* source = playout_audio_buffer_.get();
  fine_audio_buffer_->GetPlayoutData(source);
  memcpy(destination, source, dataSizeInBytes);
  return noErr;
}

}  // namespace webrtc
