iOS Audio Unit(一)

最近一直在做iOS音频相关技术的项目,期间在官方及网上的资料文档也学习了很多,当然,iOS平台中音频相关技术还是有很多方面的,这里我先总体概述下,然后以I/O Audio Unit为例对其概念,基本用法和思路进行讲解,可能不够全面,一些细节需要自行查找相关文档。后面我会对github上一个开源的音频引擎框架进行源码分析,来展现在更复杂的音频技术应用场景下可能的设计及实现方式。

本文图片及大部分技术概念阐述均来自apple官网

Core Audio

Core Audio 是iOS和MAC系统中的关于数字音频处理的基础设施,它是应用程序用来处理音频的一组软件框架,所有关于iOS音频开发的接口都是由Core Audio来提供或者经过它提供的接口来进行封装的。Apple官方对Core Audio的框架分层图示如下:

core_audio_layers.png

Low-Level

该主要在MAC上的音频APP实现中并且需要最大限度的实时性能的情况下使用,大部分音频APP不需要使用该层的服务。而且,在iOS上也提供了具备较高实时性能的高层API达到你的需求。例如OpenAL,在游戏中具备与I/O直接调用的实时音频处理能力
I/O Kit, 与硬件驱动交互
Audio HAL, 音频硬件抽象层,使API调用与实际硬件相分离,保持独立
Core MIDI, 为MIDI流和设备提供软件抽象工作层
Host Time Services, 访问电脑硬件时钟

Mid-Level

该层功能比较齐全,包括音频数据格式转换,音频文件读写,音频流解析,插件工作支持等
Audio Convert Services 负责音频数据格式的转换
Audio File Services 负责音频数据的读写
Audio Unit ServicesAudio Processing Graph Services 支持均衡器和混音器等数字信号处理的插件
Audio File Scream Services 负责流解析
Core Audio Clock Services 负责音频音频时钟同步

High-Level

是一组从低层接口组合起来的高层应用,基本上我们很多关于音频开发的工作在这一层就可以完成
Audio Queue Services 提供录制、播放、暂停、循环、和同步音频它自动采用必要的编解码器处理压缩的音频格式
AVAudioPlayer 是专为IOS平台提供的基于Objective-C接口的音频播放类,可以支持iOS所支持的所有音频的播放
Extended Audio File Services 由Audio File与Audio Converter组合而成,提供压缩及无压缩音频文件的读写能力
OpenAL 是CoreAudio对OpenAL标准的实现,可以播放3D混音效果

不同场景所需要的API Service

  • 只实现音频的播放,没有其他需求,AVAudioPlayer就可以满足需求。它的接口使用简单,不用关心其中的细节,通常只提供给它一个播放源的URL地址,并且调用其play、pause、stop等方法进行控制,observer其播放状态更新UI即可

  • APP需要对音频进行流播放,就需要AudioFileStreamer加Audio Queue,将网络或者本地的流读取到内存,提交给AudioFileStreamer解析分离音频帧,分离出来的音频帧可以送给AudioQueue进行解码和播放
    可参考
    AudioStreamer
    FreeStreamer
    AFSoundManager

  • APP需要需要对音频施加音效(均衡器、混响器),就是除了数据的读取和解析以外还需要用到AudioConverter或者Codec来把音频数据转换成PCM数据,再由AudioUnit+AUGraph来进行音效处理和播放
    可参考
    DouAudioStreamer
    TheAmazingAudioEngine
    AudioKit

Audio Unit

iOS提供了混音、均衡、格式转换、实时IO录制、回放、离线渲染、语音对讲(VoIP)等音频处理插件,它们都属于不同的AudioUnit,支持动态载入和使用。AudioUnit可以单独创建使用,但更多的是被组合使用在Audio Processing Graph容器中以达到多样的处理需要,例如下面的一种场景:

AboutAudioUnitHosting_2x.png

APP持有的Audio Processing Graph容器中包含两个EQ Unit、一个Mixer Unit、一个I/O Unit,APP将磁盘或者网络中的两路流数据分别通过EQ Unit进行均衡处理,然后在Mixer Unit经过混音处理为一路,进入I/O Unit将此路数据送往硬件去播放。在这整个流程中,APP随时可以调整设置AU Graph及其中每个Unit的工作状态及参数,动态性的接入或者移出指定的Unit,并且保证线程安全。

Audio Unit类型:

I/O: Remote I/O、Voice-Processing I/O、Generic Output
Mixing: 3D Mixer、Mutichannel Mixer
Effect: iPod Equalizer
Format Conversion: Format Converter

AudioUnit构建方式

创建Audio Unit有两种途径,以I/O Unit为例,一种是直接调用unit接口创建,一种是通过Audio Unit Graph创建,下面是两种创建方式的基本流程和相关代码:

Unit API方式(Remote IO Unit)

    // create IO Unit
    BOOL result = NO;
    AudioComponentDescription outputDescription = {0};
    outputDescription.componentType = kAudioUnitType_Output;
    outputDescription.componentSubType = kAudioUnitSubType_RemoteIO;
    outputDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    outputDescription.componentFlags = 0;
    outputDescription.componentFlagsMask = 0;
    AudioComponent comp = AudioComponentFindNext(NULL, &outputDescription);
    result = CheckOSStatus(AudioComponentInstanceNew(comp, &mVoipUnit), @"couldn't create a new instance of RemoteIO");
    if (!result) return result;
    
    // config IO Enable status
    UInt32 flag = 1;
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &flag, sizeof(flag)), @"could not enable output on RemoteIO");
    if (!result) return result;
    
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &flag, sizeof(flag)),                  @"AudioUnitSetProperty EnableIO");
    if (!result) return result;
    
    // Config default format
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &inputAudioDescription, sizeof(inputAudioDescription)), @"couldn't set the input client format on RemoteIO");
    if (!result) return result;
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &outputAudioDescription, sizeof(outputAudioDescription)), @"couldn't set the output client format on RemoteIO");
    if (!result) return result;
    
    // Set the MaximumFramesPerSlice property. This property is used to describe to an audio unit the maximum number
    // of samples it will be asked to produce on any single given call to AudioUnitRender
    UInt32 maxFramesPerSlice = 4096;
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFramesPerSlice, sizeof(UInt32)), @"couldn't set max frames per slice on RemoteIO");
    if (!result) return result;
    
    // Set the record callback
    AURenderCallbackStruct recordCallback;
    recordCallback.inputProc = recordCallbackFunc;
    recordCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, kInputBus, &recordCallback, sizeof(recordCallback)), @"couldn't set record callback on RemoteIO");
    if (!result) return result;
    
    // Set the playback callback
    AURenderCallbackStruct playbackCallback;
    playbackCallback.inputProc = playbackCallbackFunc;
    playbackCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, kOutputBus, &playbackCallback, sizeof(playbackCallback)), @"couldn't set playback callback on RemoteIO");
    if (!result) return result;
    
    // set buffer allocate
    flag = 0;
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
                                                kAudioUnitProperty_ShouldAllocateBuffer,
                                                kAudioUnitScope_Output,
                                                kInputBus,
                                                &flag,
                                                sizeof(flag)), @"couldn't set property for ShouldAllocateBuffer");
    if (!result) return result;
    
    // Initialize the output IO instance
    result = CheckOSStatus(AudioUnitInitialize(mVoipUnit), @"couldn't initialize VoiceProcessingIO instance");
    if (!result) return result;
    
    return YES;

AU Graph方式(MultiChannelMixer Unit + Remote IO Unit)

    // create AUGraph
    BOOL result = NO;
    result = CheckOSStatus(NewAUGraph (&processingGraph), @"couldn't create a new instance of AUGraph");
    if (!result) return result;
    
    // I/O unit
    AudioComponentDescription iOUnitDescription;
    iOUnitDescription.componentType          = kAudioUnitType_Output;
    iOUnitDescription.componentSubType       = kAudioUnitSubType_RemoteIO;
    iOUnitDescription.componentManufacturer  = kAudioUnitManufacturer_Apple;
    iOUnitDescription.componentFlags         = 0;
    iOUnitDescription.componentFlagsMask     = 0;
    
    // Multichannel mixer unit
    AudioComponentDescription MixerUnitDescription;
    MixerUnitDescription.componentType          = kAudioUnitType_Mixer;
    MixerUnitDescription.componentSubType       = kAudioUnitSubType_MultiChannelMixer;
    MixerUnitDescription.componentManufacturer  = kAudioUnitManufacturer_Apple;
    MixerUnitDescription.componentFlags         = 0;
    MixerUnitDescription.componentFlagsMask     = 0;
    
    AUNode   iONode;         // node for I/O unit
    AUNode   mixerNode;      // node for Multichannel Mixer unit
    
    result = CheckOSStatus(AUGraphAddNode (
                                           processingGraph,
                                           &iOUnitDescription,
                                           &iONode), @"couldn't add a node instance of kAudioUnitSubType_RemoteIO");
    if (!result) return result;
    
    result = CheckOSStatus(AUGraphAddNode (
                                           processingGraph,
                                           &MixerUnitDescription,
                                           &mixerNode), @"couldn't add a node instance of mixer unit");
    if (!result) return result;
    
    // open the AUGraph
    result = CheckOSStatus(AUGraphOpen (processingGraph), @"couldn't get instance of mixer unit");
    if (!result) return result;
    
    // Obtain unit instance
    result = CheckOSStatus(AUGraphNodeInfo (
                                            processingGraph,
                                            mixerNode,
                                            NULL,
                                            &mMixerUnit
                                            ), @"couldn't get instance of mixer unit");
    if (!result) return result;
    
    result = CheckOSStatus(AUGraphNodeInfo (
                                            processingGraph,
                                            iONode,
                                            NULL,
                                            &mVoipUnit
                                            ), @"couldn't get a new instance of remoteio unit");
    if (!result) return result;
    
    /////////////////////////////////////////////////////////////////////////////////////////
    
    UInt32 busCount   = 2;    // bus count for mixer unit input
    UInt32 guitarBus  = 0;    // mixer unit bus 0 will be stereo and will take the guitar sound
    UInt32 beatsBus   = 1;    // mixer unit bus 1 will be mono and will take the beats sound
    result = CheckOSStatus(AudioUnitSetProperty (
                                                 mMixerUnit,
                                                 kAudioUnitProperty_ElementCount,
                                                 kAudioUnitScope_Input,
                                                 0,
                                                 &busCount,
                                                 sizeof (busCount)
                                                 ), @"could not set mixer unit input bus count");
    if (!result) return result;
    
    UInt32 maximumFramesPerSlice = 4096;
    result = CheckOSStatus(AudioUnitSetProperty (
                                                 mMixerUnit,
                                                 kAudioUnitProperty_MaximumFramesPerSlice,
                                                 kAudioUnitScope_Global,
                                                 0,
                                                 &maximumFramesPerSlice,
                                                 sizeof (maximumFramesPerSlice)
                                                 ), @"could not set mixer unit maximum frame per slice");
    if (!result) return result;
    
    
    // Attach the input render callback and context to each input bus
    for (UInt16 busNumber = 0; busNumber < busCount; ++busNumber) {
        
        // Setup the struture that contains the input render callback
        AURenderCallbackStruct playbackCallback;
        playbackCallback.inputProc = playbackCallbackFunc;
        playbackCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
        
        NSLog (@"Registering the render callback with mixer unit input bus %u", busNumber);
        // Set a callback for the specified node's specified input
        result = CheckOSStatus(AUGraphSetNodeInputCallback (
                                                            processingGraph,
                                                            mixerNode,
                                                            busNumber,
                                                            &playbackCallback
                                                            ), @"couldn't set playback callback on mixer unit");
        if (!result) return result;
    }
    
    
    // Config mixer unit input default format
    result = CheckOSStatus(AudioUnitSetProperty (
                                                 mMixerUnit,
                                                 kAudioUnitProperty_StreamFormat,
                                                 kAudioUnitScope_Input,
                                                 guitarBus,
                                                 &outputAudioDescription,
                                                 sizeof (outputAudioDescription)
                                                 ), @"couldn't set the input 0 client format on mixer unit");
    if (!result) return result;
    
    result = CheckOSStatus(AudioUnitSetProperty (
                                                 mMixerUnit,
                                                 kAudioUnitProperty_StreamFormat,
                                                 kAudioUnitScope_Input,
                                                 beatsBus,
                                                 &outputAudioDescription,
                                                 sizeof (outputAudioDescription)
                                                 ), @"couldn't set the input 1 client format on mixer unit");
    if (!result) return result;
    
    Float64 graphSampleRate = 44100.0;    // Hertz;
    result = CheckOSStatus(AudioUnitSetProperty (
                                                 mMixerUnit,
                                                 kAudioUnitProperty_SampleRate,
                                                 kAudioUnitScope_Output,
                                                 0,
                                                 &graphSampleRate,
                                                 sizeof (graphSampleRate)
                                                 ), @"couldn't set the output client format on mixer unit");
    if (!result) return result;
    
    ////////////////////////////////////////////////////////////////////////////////////////////
    
    // config void unit IO Enable status
    UInt32 flag = 1;
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
                                                kAudioOutputUnitProperty_EnableIO,
                                                kAudioUnitScope_Output,
                                                kOutputBus,
                                                &flag,
                                                sizeof(flag)
                                                ), @"could not enable output on kAudioUnitSubType_RemoteIO");
    if (!result) return result;
    
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
                                                kAudioOutputUnitProperty_EnableIO,
                                                kAudioUnitScope_Input,
                                                kInputBus,
                                                &flag,
                                                sizeof(flag)
                                                ), @"could not enable input on kAudioUnitSubType_RemoteIO");
    if (!result) return result;
    
    // config voip unit default format
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
                                                kAudioUnitProperty_StreamFormat,
                                                kAudioUnitScope_Output,
                                                kInputBus,
                                                &inputAudioDescription,
                                                sizeof(inputAudioDescription)
                                                ), @"couldn't set the input client format on kAudioUnitSubType_RemoteIO");
    if (!result) return result;
    

    UInt32 maxFramesPerSlice = 4096;
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
                                                kAudioUnitProperty_MaximumFramesPerSlice,
                                                kAudioUnitScope_Global,
                                                0,
                                                &maxFramesPerSlice,
                                                sizeof(UInt32)
                                                ), @"couldn't set max frames per slice on kAudioUnitSubType_RemoteIO");
    if (!result) return result;
    
    // Set the record callback
    AURenderCallbackStruct recordCallback;
    recordCallback.inputProc = recordCallbackFunc;
    recordCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
                                                kAudioOutputUnitProperty_SetInputCallback,
                                                kAudioUnitScope_Global,
                                                kInputBus,
                                                &recordCallback,
                                                sizeof(recordCallback)
                                                ), @"couldn't set record callback on kAudioUnitSubType_RemoteIO");
    if (!result) return result;
    
    // set buffer allocate
    flag = 0;
    result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
                                                kAudioUnitProperty_ShouldAllocateBuffer,
                                                kAudioUnitScope_Output,
                                                kInputBus,
                                                &flag,
                                                sizeof(flag)), @"couldn't set property for ShouldAllocateBuffer");
    if (!result) return result;
    
    /////////////////////////////////////////////////////////////////////////////////////////////
    // Initialize the output IO instance
    result = CheckOSStatus(AUGraphConnectNodeInput (
                                                    processingGraph,
                                                    mixerNode,         // source node
                                                    0,                 // source node output bus number
                                                    iONode,            // destination node
                                                    0                  // desintation node input bus number
                                                    ), @"couldn't connect ionode to mixernode");
    if (!result) return result;
    
    result = CheckOSStatus(AUGraphInitialize (processingGraph), @"AUGraphInitialize failed");
    if (!result) return result;

    return YES;

AudioUnit数据的输入输出方式

Unit处理音频数据,都要经过一个输入和输出过程,设置输入输出的音频格式(可以相同或者不同),两个Unit对接即是将一个Unit的输入接到另一个Unit的输出,或者将一个Unit的输出接到另一个Unit的输入,需要注意的是在对接点要保证Audio Format的一致性。以Remote I/O Unit为例,结构如下图所示:

IO_unit_2x.png

一个I/O Unit包含两个实体对象,两个实体对象(Element 0、Element 1)相互独立,根据需求可通过kAudioOutputUnitProperty_EnableIO属性去开关它们。Element 1与硬件输入连接,并且Element 1的输入域(input scope)对你不可见,你只能读取它的输出域的数据及设置其输出域的音频格式;Element 0与硬件输出连接,并且Element 0的输出域(ouput scope)对你不可见,你只能写入它的输入域的数据及设置其输入域的音频格式。

如何将输入设备采集的数据抓出来,又如何将处理后的数据送到输出设备呢?
通过AURenderCallbackStruct结构,将定义的两个回调静态方法地址设置到需要的Element 0/1上,当Unit配置完毕并且运行后,Unit调度线程会按照当前设备状态及音频格式安排调度周期,循环往复的调用你提供的录制与播放回调方法,样例代码如下:

// for record callback, read audio data from bufferlist
static OSStatus recordCallbackFunc(void *inRefCon,
                                  AudioUnitRenderActionFlags *ioActionFlags,
                                  const AudioTimeStamp *inTimeStamp,
                                  UInt32 inBusNumber,
                                  UInt32 inNumberFrames,
                                  AudioBufferList *ioData){

ASAudioEngineSingleU *engine = (__bridge ASAudioEngineSingleU* )inRefCon;

OSStatus err = noErr;
if (engine.audioChainIsBeingReconstructed == NO){
    
    @autoreleasepool {
        AudioBufferList bufList = [engine getBufferList:inNumberFrames];
        err = AudioUnitRender([engine recorderUnit], ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &bufList);
        if (err) {
            HMLogDebug(LogModuleAudio, @"AudioUnitRender error code = %d", err);
        } else {
            AudioBuffer buffer = bufList.mBuffers[0];
            NSData *pcmBlock = [NSData dataWithBytes:buffer.mData length:buffer.mDataByteSize];
            [engine didRecordData:pcmBlock];
        }
    }
}

return err;
}

// for play callback, fill audio data to bufferlist
static OSStatus playbackCallbackFunc(void *inRefCon,
                             AudioUnitRenderActionFlags *ioActionFlags,
                             const AudioTimeStamp *inTimeStamp,
                             UInt32 inBusNumber,
                             UInt32 inNumberFrames,
                             AudioBufferList *ioData){

ASAudioEngineSingleU *engine = (__bridge ASAudioEngineSingleU* )inRefCon;

OSStatus err = noErr;
if (engine.audioChainIsBeingReconstructed == NO)
{
    for (int i = 0; i < ioData -> mNumberBuffers; i++) {
        @autoreleasepool {
            AudioBuffer buffer = ioData -> mBuffers[i];
            NSData *pcmBlock = [engine getPlayFrame:buffer.mDataByteSize];
            if (pcmBlock && pcmBlock.length) {
                UInt32 size = (UInt32)MIN(buffer.mDataByteSize, [pcmBlock length]);
                memcpy(buffer.mData, [pcmBlock bytes], size);
                buffer.mDataByteSize = size;
                //HMLogDebug(LogModuleAudio, @"AudioUnitRender pcm data has filled");
            } else {
                buffer.mDataByteSize = 0;
                *ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
            }
        } // end pool
    } // end for
} // end if

return err;

}

不同场景下AudioUnit构建样例

I/O 无渲染

从输入设备采集过来的数据,先经过MutilChannelMixer Unit,再送到输出设备播放,该构建方式在于中间的Unit可对mic采集采集过来的数据进行声相调节以及音量的调节

IOWithoutRenderCallback_2x.png

I/O 有渲染

该构建方式在输入与输出之间增加了rendercallback,可以在硬件采集过来的数据上做一些处理(例如,增益、调制、音效等)后再送到输出播放

IOWithRenderCallback_2x.png

仅输出并且带渲染

适合音乐游戏及合成器类的APP,仅使用IO Unit的output端,在rendercallback中负责播放源的提取整理并准备送播,比较简单的构建方式

OutputOnlyWithRenderCallback_2x.png

较复杂的构建

输入端有两路音频流,都是通过rendercallback方式抓取数据,其中一路音频流直接给入到Mixer Unit中,另一路先经过EQ Unit处理后给入到Mixer Unit中,

OutputOnlyWithRenderCallbackExtended_2x.png

Tips

1. 多线程及内存管理

尽可能的避免render callback方法内做加锁及处理耗时较高的操作,这样可以最大限度的提升实时性能,如果播放数据或者采集数据存在不同线程读写的情况,必需要加锁保护,推荐pthread相关lock方法性能比其它锁要高
音频的输入输出一般都是一个持续的过程,在采集与播放的callback中,应尽量复用buffer及避免多次buffer拷贝,而不是每次回调都重新申请和释放,在适当的位置加上@autoreleasepool避免长时间运行内存不断上涨

2. 格式

Core Audio Type中定义了AudioStreamBasicDescription结构,Audio Unit及其它很多音频API对格式的配置都需要用到它,根据需要将该结构的信息填充正确,下面是44.1K,stereo,16bit的填充例子

audioDescription.mSampleRate = 44100;
audioDescription.mChannelsPerFrame = 2;
audioDescription.mBitsPerChannel = 16;

audioDescription.mFramesPerPacket = 1;
audioDescription.mFormatID = kAudioFormatLinearPCM;
audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;

audioDescription.mBytesPerFrame = (audioDescription.mBitsPerChannel/8) * audioDescription.mChannelsPerFrame;
audioDescription.mBytesPerPacket = audioDescription.mBytesPerFrame ;

苹果官方建议在整个Audio Processing Graph或者Unit之间尽量以相同的音频格式流通,尽管Audio Unit的输入输出可以不同。另外在Unit之间输入输出连接点要保持一致。

3. 音质

在使用过程中,Audio Unit的format是可以动态改变的,但存在一种情况,Unit在销毁前最好恢复到默认创建时的format,否则在销毁后再重建Unit后,可能出现播放音质变差(音量变小,声音粗糙)的情况。
在使用VoiceProcessing I/O Unit过程,遇到在有些iphone上开启扬声器后,Unit从Mic采集过来的数据为空或者噪音的情况,从APP STORE中下载了其它的VOIP类型的APP也同样存在该问题,后来将AudioUnitSubType改成RemoteIO类型后,问题消失,怀疑苹果在VoiceProcessing Unit上对回声消除功能的处理上有bug

4. AudioSession

既然使用了音频特性,就会用到AudioSession,随着功能需求跟进,与它相关的问题也瞒多的,比如路由管理(听筒扬声器、线控耳机、蓝牙耳机),打断处理(interruption、iphone call)等,这里以Audio Unit为主,就不对它进行详细描述了,需要注意的是

  1. 音频的路由变更(用户挺拔耳机,或者代码调用强制切换)涉及到iOS硬件上输入和输出设备的改变,I/O类型Unit的采集和播放线程在切换过程中会阻塞一定时间(200ms左右),如果是语音对讲类对实时性要求较高的应用场景要考虑丢包策略。
  2. 在APP前台工作时,iPhone来电或者用户主动切换到其它音频类APP后,要及时处理音频的打断机制,在恰当的时机停止及恢复Unit的工作,由于iOS平台对资源的独占方式,iPhone在通话等操作时,APP中的Unit是无法初始化或者继续工作的。

推荐阅读更多精彩内容