AudioUnit播放PCM文件(三)

前言:

AudioUnit是什么?在IOS平臺下,AudioUnit是一個底層音頻處理框架,主要功能如下:
1、音頻播放與錄制
2、音頻數(shù)據(jù)格式的解析與保存
3、多路音頻的合成
4、音頻中的回聲消除
5、音頻特效如均衡器、壓縮器、混響器
它的功能遠不止這些,同時它具有較低的處理延時,所有的音頻渲染框架的底層都是基于AudioUnit實現(xiàn)的,比較高層次的框架如MediaPlayer,AVFoundation, OpenAL和Audio Toolbox;


image.png

AudioUnit系列文章

AudioUnit之-播放裸PCM音頻文件(一)
AudioUnit之-錄制音頻+耳返(二)
AudioUnit之-錄制音頻保存為m4a/CAF/WAV文件和播放m4a/CAF/WAV文件(三)
AudioUnit之-錄制音頻并添加背景音樂(四)
AudioUnit之-generic output(離線)混合音頻文件(五)

音頻相關(guān)知識

1、IOS支持的音頻編碼格式

格式定義在CoreAudio/CoreAudioTypes.h頭文件的枚舉AudioFormatID中
1、常見未壓縮編碼格式:LPCM(標(biāo)準(zhǔn)的線性脈沖編碼,未壓縮),IMA4;
2、常見壓縮編碼格式:aac,mp3,AMR等
3、多種LPCM的變體:對于LPCM音頻編碼是iPhone中使用非壓縮音頻數(shù)據(jù)最好的數(shù)據(jù)格式.同時,根據(jù)具體的存儲方式,又有多種變種.音頻數(shù)據(jù)可以存儲于大端或者小端模式,用float或者integer存儲,也可以使用不同的bit-width存儲.而在iPhone中,使用的最平凡的是:little-endian integer 16bit(或者LEI16 short類型)的格式.

2、IOS常用音頻容器格式

iPhone支持許多文件格式(音頻容器)包括:MPEG-1(.mp3),MPEG-2 ADTS(.aac),AIFF,CAF,WAVE等.但是通常在iPhone中使用的容器格式就CAF,因為它可以用來封裝iPhone所支持的所有音頻格式.

使用AudioUnit播放音頻

相關(guān)頭文件:
#import<AVFoundation/AVFoundation.h>
#import<AudioToolbox/AudioToolbox.h>
#import<CoreAudio/CoreAudioTypes.h>

AudioUnit一端連接著揚聲器,另一端連接著應(yīng)用端,應(yīng)用端不停的將PCM音頻數(shù)據(jù)輸入給AudioUnit就實現(xiàn)了音頻的播放,使用AudioUnit來播放PCM音頻數(shù)據(jù)的步驟如下:
1、創(chuàng)建音頻會話

// 1、創(chuàng)建一個音頻會話 它是單例;AVAudioSession 在AVFoundation/AVFAudio/AVAudioSession.h中定義
_aSession = [AVAudioSession sharedInstance];
        
//  2、======配置音頻會話 ======//
/** 配置使用的音頻硬件:
 *  AVAudioSessionCategoryPlayback:只是進行音頻的播放(只使用聽的硬件,比如手機內(nèi)置喇叭,或者通過耳機)
 *  AVAudioSessionCategoryRecord:只是采集音頻(只錄,比如手機內(nèi)置麥克風(fēng))
 *  AVAudioSessionCategoryPlayAndRecord:一邊采集一遍播放(聽和錄同時用)
 */
[_aSession setCategory:category error:nil];
// 設(shè)置采樣率,不管是播放還是錄制聲音 都需要設(shè)置采樣率
[_aSession setPreferredSampleRate:rate error:nil];
        
// 設(shè)置I/O的Buffer,數(shù)值越小說明緩存的數(shù)據(jù)越小,延遲也就越低;這里意思就是麥克風(fēng)采集聲音時只緩存20ms的數(shù)據(jù)
[_aSession setPreferredIOBufferDuration:duration error:nil];
// 激活會話
[_aSession setActive:YES error:nil];

2.創(chuàng)建AudioUnit描述組件
這里主要是定義AudioUnit的類型,定義在AudioToolbox/AUComponent.h文件中

AudioUnit的類型,定義在AudioToolbox/AUComponent.h文件中
CF_ENUM(UInt32) {
kAudioUnitType_Output = 'auou',
kAudioUnitType_MusicDevice = 'aumu',
kAudioUnitType_MusicEffect = 'aumf',
kAudioUnitType_FormatConverter = 'aufc',
kAudioUnitType_Effect = 'aufx',
kAudioUnitType_Mixer = 'aumx',
kAudioUnitType_Panner = 'aupn',
kAudioUnitType_Generator = 'augn',
kAudioUnitType_OfflineEffect = 'auol',
kAudioUnitType_MIDIProcessor = 'aumi'
};
1、kAudioUnitType_Effect;主要用于提供聲音特效的處理,包括的子類型有
均衡效果器:kAudioUnitSubType_NBandEQ,用于為聲音的某些頻帶增強或減弱能量
壓縮效果器:kAudioUnitSubType_DynamicsProcessor,增大或者減少音量
混響效果器:kAudioUnitSubType_Reverb2,提供混響效果
2、kAudioUnitType_Mixer:提供Mix多路聲音功能
多路混音效果器:kAudioUnitSubType_MultiChannelMixer,可以接受多路音頻的輸入,然后分別調(diào)整每一路音頻的增益與開關(guān),并將多路音頻合成一路
3、kAudioUnitType_Output:提供音頻的錄制,播放功能
錄制和播放音頻:kAudioUnitSubType_RemoteIO,后面通過AudioUnitSetProperty()方法具體是訪問麥克風(fēng)還是揚聲器
訪問音頻數(shù)據(jù):kAudioUnitSubType_GenericOutput
4、kAudioUnitType_FormatConverter:提供音頻格式轉(zhuǎn)化功能,比如采樣率轉(zhuǎn)換,聲道數(shù)轉(zhuǎn)換,采樣格式轉(zhuǎn)化,panner到packet轉(zhuǎn)換等等
kAudioUnitSubType_AUConverter:提供格式轉(zhuǎn)換功能
kAudioUnitSubType_AudioFilePlayer:直接從文件獲取輸入音頻數(shù)據(jù),它具有解碼功能
kAudioUnitSubType_NewTimePitch:變速變調(diào)效果器


/**
 *  與AudioUnit有關(guān)的錯誤類型枚舉定義在AudioToolbox/AUComponent.h文件中
 *  CF_ENUM(OSStatus) {
 *      kAudioUnitErr_InvalidProperty            = -10879,
 *      .......
 *  }
 */
// 創(chuàng)建指定的類型
+ (AudioComponentDescription)descriptionWithType:(OSType)type subType:(OSType)subType fucture:(OSType)manufuture
{
    AudioComponentDescription acd;
    acd.componentType = type;
    acd.componentSubType = subType;
    acd.componentManufacturer = manufuture;
    return acd;
}

3、創(chuàng)建AudioUnit:

/** 創(chuàng)建 AudioUnit
 *  和通過AUGraph創(chuàng)建;
*/
- (void)createAudioUnitByAugraph
{
    OSStatus status = noErr;
    //1、創(chuàng)建AUGraph
    status = NewAUGraph(&_aGraph);
    if (status != noErr) {
        NSLog(@"create AUGraph fail %d",status);
    }
    
    //2.2 將指定的組件描述創(chuàng)建AUNode并添加到AUGraph中
    status = AUGraphAddNode(_aGraph, &_ioDes, &_ioNode);
    if (status != noErr) {
        NSLog(@"AUGraphAddNode fail _ioDes %d",status);
    }
    status = AUGraphAddNode(_aGraph, &_cvtDes, &_cvtNode);
    if (status != noErr) {
        NSLog(@"AUGraphAddNode fail _cvtDes %d",status);
    }
    
    // 3、打開AUGraph(即初始化了AUGraph)
    status = AUGraphOpen(_aGraph);
    if (status != noErr) {
        NSLog(@"AUGraphOpen fail %d",status);
    }
    
    // 4、打開了AUGraph之后才能獲取指定的AudioUnit
    status = AUGraphNodeInfo(_aGraph, _ioNode, NULL, &_ioUnit);
    if (status != noErr) {
        NSLog(@"AUGraphNodeInfo fail %d",status);
    }
    status = AUGraphNodeInfo(_aGraph, _cvtNode, NULL, &_cvtUnit);
    if (status != noErr) {
        NSLog(@"AUGraphNodeInfo fail %d",status);
    }
    
}

4、設(shè)置AudioUnit屬性

/** 設(shè)置AudioUnit屬性
 *  1、通過AudioUnitSetProperty
 *  2、關(guān)于remoteIO的element,揚聲器對應(yīng)的AudioUnitElement值為0,app能控制的AudioUnitScope值為kAudioUnitScope_Input;麥克風(fēng)對應(yīng)的AudioUnitElement值為1
 *  app能控制的udioUnitScope值為kAudioUnitScope_Output
 */
- (void)setAudioUnitProperties
{
    // 開啟揚聲器的播放功能;注:對于揚聲器默認(rèn)是開啟的,對于麥克風(fēng)則默認(rèn)是關(guān)閉的
    uint32_t flag = 1;// 1代表開啟,0代表關(guān)閉
    OSStatus status = AudioUnitSetProperty(_ioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0, &flag, sizeof(flag));
    if (status != noErr) {
        NSLog(@"AudioUnitSetProperty fail %d",status);
    }
    
    AudioFormatFlags flags = self.aSession.formatFlags;
    CGFloat rate = self.aSession.currentSampleRate;
    NSInteger chs = self.aSession.currentChannels;
    //輸入給揚聲器的音頻數(shù)據(jù)格式
    AudioStreamBasicDescription odes = [ADUnitTool streamDesWithLinearPCMformat:kAudioFormatFlagIsFloat|kAudioFormatFlagIsNonInterleaved sampleRate:rate channels:chs];
    // PCM文件的音頻的數(shù)據(jù)格式
    AudioStreamBasicDescription cvtInDes = [ADUnitTool streamDesWithLinearPCMformat:flags sampleRate:rate channels:chs];
    
    // 設(shè)置揚聲器的輸入音頻數(shù)據(jù)格式
    status = AudioUnitSetProperty(_ioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &odes, sizeof(odes));
    if (status != noErr) {
        NSLog(@"AudioUnitSetProperty io fail %d",status);
    }
    
    // 設(shè)置格式轉(zhuǎn)換器的輸入輸出音頻數(shù)據(jù)格式;對于格式轉(zhuǎn)換器AudioUnit 他的AudioUnitElement只有一個 element0
    status = AudioUnitSetProperty(_cvtUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &cvtInDes, sizeof(cvtInDes));
    if (status != noErr) {
        NSLog(@"AudioUnitSetProperty convert in fail %d",status);
    }
    status = AudioUnitSetProperty(_cvtUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &odes, sizeof(odes));
    if (status != noErr) {
        NSLog(@"AudioUnitSetProperty convert ou fail %d",status);
    }
    
    /** 構(gòu)建連接
     *  只有構(gòu)建連接之后才有一個完整的數(shù)據(jù)驅(qū)動鏈。如下將構(gòu)成鏈條如下:
     *  _cvtUnit通過回調(diào)向文件要數(shù)據(jù),得到數(shù)據(jù)后進行格式轉(zhuǎn)換,將輸出作為輸入數(shù)據(jù)輸送給_ioUnit,然后_ioUnit播放數(shù)據(jù)
     */
    status = AUGraphConnectNodeInput(_aGraph, _cvtNode, 0, _ioNode, 0);
    if (status != noErr) {
        NSLog(@"AUGraphConnectNodeInput fail %d",status);
    }
    AURenderCallbackStruct callback;
    callback.inputProc = InputRenderCallback;
    callback.inputProcRefCon = (__bridge void*)self;
    status = AudioUnitSetProperty(_cvtUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callback, sizeof(callback));
    if (status != noErr) {
        NSLog(@"AudioUnitSetProperty fail %d",status);
    }
}

tips:根據(jù)步驟4的代碼,總結(jié),
AudioUnit播放音頻的數(shù)據(jù)驅(qū)動鏈條為:
_cvtUnit通過回調(diào)向文件要數(shù)據(jù),得到數(shù)據(jù)后進行格式轉(zhuǎn)換,將輸出作為輸入數(shù)據(jù)輸送給_ioUnit,然后_ioUnit播放數(shù)據(jù)

5、播放

- (void)play
{
    OSStatus stauts;
    CAShow(_aGraph);
    
    // 7、初始化AUGraph,初始化之后才能正常啟動播放
    stauts = AUGraphInitialize(_aGraph);
    if (stauts != noErr) {
        NSLog(@"AUGraphInitialize fail %d",stauts);
    }
    stauts = AUGraphStart(_aGraph);
    if (stauts != noErr) {
        NSLog(@"AUGraphStart fail %d",stauts);
    }
}

項目地址:

Demo

image.png

具體的代碼實現(xiàn)在文件ADAudioUnitPlay.h/.m中

溫馨小提示:
由于代碼混合在一塊了,后面多了對ffmpeg的代碼整合,使用了git-lfs的文件傳輸功能,所以git clone之前需要先安裝git-lfs,如下:

1、安裝homebrew(如果沒有) ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
2、安裝git-lfs brew install git-lfs
3、git clone https://github.com/nldzsz/media-ios.git

否則會提示
"Undefined symbol: _av。。。之類"

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容