Audio Unit (一)

音視頻開發t哥 發佈 2024-03-04T17:17:47.218145+00:00

例如OpenAL,在遊戲中具備與I/O直接調用的實時音頻處理能力 I/O Kit, 與硬體驅動交互 Audio HAL, 音頻硬體抽象層,使API調用與實際硬體相分離,保持獨立 Core MIDI, 為MIDI流和設備提供軟體抽象工作層 Host Time Services, 訪問電腦硬體時鐘。

最近一直在做iOS音頻相關技術的項目,期間在官方及網上的資料文檔也學習了很多,當然,iOS平台中音頻相關技術還是有很多方面的,這裡我先總體概述下,然後以I/O Audio Unit為例對其概念,基本用法和思路進行講解,可能不夠全面,一些細節需要自行查找相關文檔。後面我會對github上一個開源的音頻引擎框架進行源碼分析,來展現在更複雜的音頻技術應用場景下可能的設計及實現方式。

本文圖片及大部分技術概念闡述均來自apple官網

1、Core Audio

Core Audio 是iOS和MAC系統中的關於數字音頻處理的基礎設施,它是應用程式用來處理音頻的一組軟體框架,所有關於iOS音頻開發的接口都是由Core Audio來提供或者經過它提供的接口來進行封裝的。Apple官方對Core Audio的框架分層圖示如下:

2、Low-Level

該主要在MAC上的音頻APP實現中並且需要最大限度的實時性能的情況下使用,大部分音頻APP不需要使用該層的服務。而且,在iOS上也提供了具備較高實時性能的高層API達到你的需求。例如OpenAL,在遊戲中具備與I/O直接調用的實時音頻處理能力 I/O Kit, 與硬體驅動交互 Audio HAL, 音頻硬體抽象層,使API調用與實際硬體相分離,保持獨立 Core MidI, 為MIDI流和設備提供軟體抽象工作層 Host Time Services, 訪問電腦硬體時鐘

3、Mid-Level

該層功能比較齊全,包括音頻數據格式轉換,音頻文件讀寫,音頻流解析,插件工作支持等 Audio Convert Services 負責音頻數據格式的轉換 Audio File Services 負責音頻數據的讀寫 Audio Unit Services Audio Processing Graph Services 支持均衡器和混音器等數位訊號處理的插件 Audio File Scream Services 負責流解析 Core Audio Clock Services 負責音頻音頻時鐘同步

4、High-Level

是一組從低層接口組合起來的高層應用,基本上我們很多關於音頻開發的工作在這一層就可以完成 Audio Queue Services 提供錄製、播放、暫停、循環、和同步音頻它自動採用必要的編解碼器處理壓縮的音頻格式 AVAudioPlayer 是專為IOS平台提供的基於Objective-C接口的音頻播放類,可以支持iOS所支持的所有音頻的播放 Extended Audio File Services 由Audio File與Audio Converter組合而成,提供壓縮及無壓縮音頻文件的讀寫能力 OpenAL 是CoreAudio對OpenAL標準的實現,可以播放3D混音效果

5、不同場景所需要的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

6、Audio Unit

iOS提供了混音、均衡、格式轉換、實時IO錄製、回放、離線渲染、語音對講(VoIP)等音頻處理插件,它們都屬於不同的AudioUnit,支持動態載入和使用。AudioUnit可以單獨創建使用,但更多的是被組合使用在Audio Processing Graph容器中以達到多樣的處理需要,例如下面的一種場景:

APP持有的Audio Processing Graph容器中包含兩個EQ Unit、一個Mixer Unit、一個I/O Unit,APP將磁碟或者網絡中的兩路流數據分別通過EQ Unit進行均衡處理,然後在Mixer Unit經過混音處理為一路,進入I/O Unit將此路數據送往硬體去播放。在這整個流程中,APP隨時可以調整設置AU Graph及其中每個Unit的工作狀態及參數,動態性的接入或者移出指定的Unit,並且保證線程安全。

C++音視頻學習資料免費獲取方法:關注音視頻開發T哥,點擊「連結」即可免費獲取2023年最新C++音視頻開發進階獨家免費學習大禮包!

6.1 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

6.2 AudioUnit構建方式

創建Audio Unit有兩種途徑,以I/O Unit為例,一種是直接調用unit接口創建,一種是通過Audio Unit Graph創建,下面是兩種創建方式的基本流程和相關代碼:

6.3 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;

6.4 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;

6.5 AudioUnit數據的輸入輸出方式

Unit處理音頻數據,都要經過一個輸入和輸出過程,設置輸入輸出的音頻格式(可以相同或者不同),兩個Unit對接即是將一個Unit的輸入接到另一個Unit的輸出,或者將一個Unit的輸出接到另一個Unit的輸入,需要注意的是在對接點要保證Audio Format的一致性。以Remote I/O Unit為例,結構如下圖所示:

一個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;

7、不同場景下AudioUnit構建樣例

7.1 I/O 無渲染

從輸入設備採集過來的數據,先經過MutilChannelMixer Unit,再送到輸出設備播放,該構建方式在於中間的Unit可對mic採集採集過來的數據進行聲相調節以及音量的調節

7.2 I/O 有渲染

該構建方式在輸入與輸出之間增加了rendercallback,可以在硬體採集過來的數據上做一些處理(例如,增益、調製、音效等)後再送到輸出播放

7.3 僅輸出並且帶渲染

適合音樂遊戲及合成器類的APP,僅使用IO Unit的output端,在rendercallback中負責播放源的提取整理並準備送播,比較簡單的構建方式

輸入端有兩路音頻流,都是通過rendercallback方式抓取數據,其中一路音頻流直接給入到Mixer Unit中,另一路先經過EQ Unit處理後給入到Mixer Unit中,

8、Tips

8.1 多線程及內存管理

儘可能的避免render callback方法內做加鎖及處理耗時較高的操作,這樣可以最大限度的提升實時性能,如果播放數據或者採集數據存在不同線程讀寫的情況,必需要加鎖保護,推薦pthread相關lock方法性能比其它鎖要高 音頻的輸入輸出一般都是一個持續的過程,在採集與播放的callback中,應儘量復用buffer及避免多次buffer拷貝,而不是每次回調都重新申請和釋放,在適當的位置加上@autoreleasepool避免長時間運行內存不斷上漲

8.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之間輸入輸出連接點要保持一致。

8.3 音質

在使用過程中,Audio Unit的format是可以動態改變的,但存在一種情況,Unit在銷毀前最好恢復到默認創建時的format,否則在銷毀後再重建Unit後,可能出現播放音質變差(音量變小,聲音粗糙)的情況。 在使用VoiceProcessing I/O Unit過程,遇到在有些iphone上開啟揚聲器後,Unit從Mic採集過來的數據為空或者噪音的情況,從APP STORE中下載了其它的VOIP類型的APP也同樣存在該問題,後來將AudioUnitSubType改成RemoteIO類型後,問題消失,懷疑蘋果在VoiceProcessing Unit上對回聲消除功能的處理上有bug

8.4 AudioSession

既然使用了音頻特性,就會用到AudioSession,隨著功能需求跟進,與它相關的問題也瞞多的,比如路由管理(聽筒揚聲器、線控耳機、藍牙耳機),打斷處理(interruption、iphone call)等,這裡以Audio Unit為主,就不對它進行詳細描述了,需要注意的是

  1. 音頻的路由變更(用戶挺拔耳機,或者代碼調用強制切換)涉及到iOS硬體上輸入和輸出設備的改變,I/O類型Unit的採集和播放線程在切換過程中會阻塞一定時間(200ms左右),如果是語音對講類對實時性要求較高的應用場景要考慮丟包策略。
  2. 在APP前台工作時,iPhone來電或者用戶主動切換到其它音頻類APP後,要及時處理音頻的打斷機制,在恰當的時機停止及恢復Unit的工作,由於iOS平台對資源的獨占方式,iPhone在通話等操作時,APP中的Unit是無法初始化或者繼續工作的。


原文連結:iOS Audio Unit (涓€) - 鎺橀噾

關鍵字: