上一篇文章中,我們針對PCM
數據,通過AudioToolBox
將PCM 數據
編碼成AAC 數據
,并把AAC 數據
添加ADTS Header
,并把AAC
格式的音頻數據寫入文件;
這一章呢,我們主要是用AudioToolBox
把AAC數據
解碼成PCM格式
,并利用AVFoundation
框架把PCM數據
從揚聲器播放處理;
1. 音頻采集
關于音頻采集部分,上篇文章已經介紹過了,是采用 AVFoundation
框架 對AVCaptureSessionSession
進行封裝,添加音頻輸入源,然后添加 output
輸出,通過采集音頻設備,最后通過代理方法拿到音頻流PCM 數據
;這里不做過多贅述 ,可以參考上篇文章AudioToolBox 編碼AAC 或者直接看源碼:https://github.com/hunter858/OpenGL_Study
2. AAC數據獲取
AAC
的原始數據,也是上篇文章介紹過的,通過 AudioEncoder
拿到的編碼后的AAC
數據部分,不包含ADTS header
部分;因為ADTS Header
主要是寫入文件需要的;
通過AudioEncoder
的audioEncodeCallback
代理方法拿到編碼后的AAC 數據;
- (void)audioEncodeCallback:(NSData *)aacData;
3. AudioToolBox 創建
關于AudioToolBox
的創建 和編碼部分一致,只是解碼部分,把input
和output
的配置 做了一個調換(這么理解);
這里我們創建一個AudioDecoder
類封裝AudioToolBox
對象,通過AudioConfig
音頻配置來初始化 硬件編碼器所需要的一些參數 ;
源碼如下:
- (void)setupEncoder {
//輸出參數pcm
AudioStreamBasicDescription outputAudioDes = {0};
outputAudioDes.mSampleRate = (Float64)_config.sampleRate; //采樣率
outputAudioDes.mChannelsPerFrame = (UInt32)_config.channelCount; //輸出聲道數
outputAudioDes.mFormatID = kAudioFormatLinearPCM; //輸出格式
outputAudioDes.mFormatFlags = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked); //編碼 12
outputAudioDes.mFramesPerPacket = 1; //每一個packet幀數 ;
outputAudioDes.mBitsPerChannel = 16; //數據幀中每個通道的采樣位數。
outputAudioDes.mBytesPerFrame = outputAudioDes.mBitsPerChannel / 8 *outputAudioDes.mChannelsPerFrame; //每一幀大小(采樣位數 / 8 *聲道數)
outputAudioDes.mBytesPerPacket = outputAudioDes.mBytesPerFrame * outputAudioDes.mFramesPerPacket; //每個packet大?。◣笮?* 幀數)
outputAudioDes.mReserved = 0; //對其方式 0(8字節對齊)
//輸入參數aac
AudioStreamBasicDescription inputAduioDes = {0};
inputAduioDes.mSampleRate = (Float64)_config.sampleRate;
inputAduioDes.mFormatID = kAudioFormatMPEG4AAC;
inputAduioDes.mFormatFlags = kMPEG4Object_AAC_LC;
inputAduioDes.mFramesPerPacket = 1024;
inputAduioDes.mChannelsPerFrame = (UInt32)_config.channelCount;
//填充輸出相關信息
UInt32 inDesSize = sizeof(inputAduioDes);
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &inDesSize, &inputAduioDes);
//獲取解碼器的描述信息(只能傳入software)
AudioClassDescription *audioClassDesc = [self getAudioCalssDescriptionWithType:outputAudioDes.mFormatID fromManufacture:kAppleSoftwareAudioCodecManufacturer];
/** 創建converter
參數1:輸入音頻格式描述
參數2:輸出音頻格式描述
參數3:class desc的數量
參數4:class desc
參數5:創建的解碼器
*/
OSStatus status = AudioConverterNewSpecific(&inputAduioDes, &outputAudioDes, 1, audioClassDesc, &_audioConverter);
if (status != noErr) {
NSLog(@"Error!:硬解碼AAC創建失敗, status= %d", (int)status);
return;
}
}
4. 解碼AAC
在通過 AudioEncoder
的 代理方法 - (void)audioEncodeCallback:(NSData *)aacData;
拿到編碼后的AAC 數據
后,直接把 AAC數據
送入解碼器,還原AAC
數據至PCM
格式;
關于解碼部分的源碼如下:
- (void)decodeAudioAACData:(NSData *)aacData {
if (!_audioConverter) { return; }
dispatch_async(_decoderQueue, ^{
//記錄aac 作為參數參入解碼回調函數
CCAudioUserData userData = {0};
userData.channelCount = (UInt32)_config.channelCount;
userData.data = (char *)[aacData bytes];
userData.size = (UInt32)aacData.length;
userData.packetDesc.mDataByteSize = (UInt32)aacData.length;
userData.packetDesc.mStartOffset = 0;
userData.packetDesc.mVariableFramesInPacket = 0;
//輸出大小和packet個數
UInt32 pcmBufferSize = (UInt32)(2048 * _config.channelCount);
UInt32 pcmDataPacketSize = 1024;
//創建臨時容器pcm
uint8_t *pcmBuffer = malloc(pcmBufferSize);
memset(pcmBuffer, 0, pcmBufferSize);
//輸出buffer
AudioBufferList outAudioBufferList = {0};
outAudioBufferList.mNumberBuffers = 1;
outAudioBufferList.mBuffers[0].mNumberChannels = (uint32_t)_config.channelCount;
outAudioBufferList.mBuffers[0].mDataByteSize = (UInt32)pcmBufferSize;
outAudioBufferList.mBuffers[0].mData = pcmBuffer;
//輸出描述
AudioStreamPacketDescription outputPacketDesc = {0};
//配置填充函數,獲取輸出數據
OSStatus status = AudioConverterFillComplexBuffer(_audioConverter, &AudioDecoderConverterComplexInputDataProc, &userData, &pcmDataPacketSize, &outAudioBufferList, &outputPacketDesc);
if (status != noErr) {
NSLog(@"Error: AAC Decoder error, status=%d",(int)status);
return;
}
//如果獲取到數據
if (outAudioBufferList.mBuffers[0].mDataByteSize > 0) {
NSData *rawData = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
dispatch_async(_callbackQueue, ^{
[_delegate audioDecodeCallback:rawData];
});
}
free(pcmBuffer);
});
}
static OSStatus AudioDecoderConverterComplexInputDataProc( AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) {
CCAudioUserData *audioDecoder = (CCAudioUserData *)(inUserData);
if (audioDecoder->size <= 0) {
ioNumberDataPackets = 0;
return -1;
}
//填充數據
*outDataPacketDescription = &audioDecoder->packetDesc;
(*outDataPacketDescription)[0].mStartOffset = 0;
(*outDataPacketDescription)[0].mDataByteSize = audioDecoder->size;
(*outDataPacketDescription)[0].mVariableFramesInPacket = 0;
ioData->mBuffers[0].mData = audioDecoder->data;
ioData->mBuffers[0].mDataByteSize = audioDecoder->size;
ioData->mBuffers[0].mNumberChannels = audioDecoder->channelCount;
return noErr;
}
5. PCM 播放
拿到PCM
數據后,如何驗證我們解碼是否成功,我們有2種辦法;
- 第一種方法是把
PCM
保存下來使用ffmpeg
進行播放; - 第二種方法是把 PCM 從揚聲器播放出來;
5.1 PCM播放(方法一):
從沙盒拿到PCM 文件后,在終端鍵入如下命令 (記得安裝ffmpeg
)
例子:ffplay -ar 44100 -ac 1 -f s16le -i /Users/pengchao/Desktop/2022_06_25_20:44:34.pcm
fplay -ar 44100 -ac 1 -f s16le -i ./201904091310_test.pcm
-ar 表示采樣率
-ac 表示音頻通道數
單聲道是 1,Android 中為 AudioFormat.CHANNEL_IN_MONO
雙聲道是 2,Android 中為 AudioFormat.CHANNEL_IN_STEREO
-f 表示 pcm 格式,sample_fmts + le(小端)或者 be(大端)
sample_fmts可以通過ffplay -sample_fmts來查詢
-i 表示輸入文件,這里就是 pcm 文件
5.2 PCM播放(方法二)
通過 AudioPCMPlayer
類,對PCM
數據進行播放,這里AudioPCMPlayer
是一個 基于 AudioQueue
的封裝;有興趣的同學可以去看源碼;
總結
源碼地址: 源碼地址 源碼地址: https://github.com/hunter858/OpenGL_Study/AVFoundation/AudiotoolBox-decoder