iOS下解码AAC并播放
- 2020 年 4 月 1 日
- 筆記
前言
今天我们介绍一下如何在iOS进行AAC解码,并使用AudioUnit播放解码后的PCM数据。
基本流程
iOS系统对音频处理做了三层封装。包括应用层、服务层和硬件层。如下图所示:

我们本次使用的都是服务层的接口。也就是上图中被红色框起来的部分。该层更接近于底层,所以灵活性更大,性能也更好。尤其对于直播相关的项目最好使用该层接口。
在iOS下进行音频解码及播放的大体流程如下:
- 打开 AAC 文件。
- 获取音频格式信息。如通道数,采样率等。
- 从 AAC 文件中取出一帧 AAC 数据。
- 使用 AudioToolbox 解码 AAC 数据包。
- 将解码后的 PCM 数据送给 AudioUnit 播放声音。
- 重复 3-5 步,直到整个 AAC 文件被读完。
下面我们对以上每一步做详细介绍。
Audio File
上面流程中第1、2、3步使用Audio File
服务。Audio File
可以用来创建、初始化音频文件;读写音频数据;对音频文件进行优化;读取和写入音频格式信息等等,功能十分强大。
我们看一下用到的几个函数原型及其参数说明。
- AudioFileOpenURL用于打开一个媒体文件。原型如下: enum { kAudioFileReadPermission = 0x01, kAudioFileWritePermission = 0x02, kAudioFileReadWritePermission = 0x03 }; extern OSStatus AudioFileOpenURL ( CFURLRef inFileRef, // 打开文件的路径 SInt8 inPermissions, // 打开文件的权限。 读/写/读写三种权限 AudioFileTypeID inFileTypeHint, // 文件类型提示信息,如果明确知道就填入,如果不知道填0. AudioFileID * outAudioFile // 文件述符 ID );
- AudioFileGetProperty 获取音视频格式信息。原型如下: enum { kAudioFilePropertyFileFormat = 'ffmt', kAudioFilePropertyDataFormat = 'dfmt', kAudioFilePropertyIsOptimized = 'optm', kAudioFilePropertyMagicCookieData = 'mgic', kAudioFilePropertyAudioDataByteCount = 'bcnt', kAudioFilePropertyAudioDataPacketCount = 'pcnt', kAudioFilePropertyMaximumPacketSize = 'psze', kAudioFilePropertyDataOffset = 'doff', kAudioFilePropertyChannelLayout = 'cmap', kAudioFilePropertyDeferSizeUpdates = 'dszu', kAudioFilePropertyMarkerList = 'mkls', kAudioFilePropertyRegionList = 'rgls', kAudioFilePropertyChunkIDs = 'chid', kAudioFilePropertyInfoDictionary = 'info', kAudioFilePropertyPacketTableInfo = 'pnfo', kAudioFilePropertyFormatList = 'flst', kAudioFilePropertyPacketSizeUpperBound = 'pkub', kAudioFilePropertyReserveDuration = 'rsrv', kAudioFilePropertyEstimatedDuration = 'edur', kAudioFilePropertyBitRate = 'brat', kAudioFilePropertyID3Tag = 'id3t', kAudioFilePropertySourceBitDepth = 'sbtd', kAudioFilePropertyAlbumArtwork = 'aart', kAudioFilePropertyAudioTrackCount = 'atct', kAudioFilePropertyUseAudioTrack = 'uatk' }; extern OSStatus AudioFileGetProperty( AudioFileID inAudioFile, //文件描述符,通过 AudioFileOpenURL 获取。 AudioFilePropertyID inPropertyID, //属性ID,如上所示 UInt32 * ioDataSize, // 输出值空间大小 void * outPropertyData //输出值地址。 );
- 从媒体文件中读取一帧数据 extern OSStatus AudioFileReadPacketData ( AudioFileID inAudioFile, // 文件描述符 Boolean inUseCache, // 是否使用cache, 一般不用 UInt32 * ioNumBytes, // 输入输出参数 AudioStreamPacketDescription * outPacketDescriptions, //输出参数 SInt64 inStartingPacket, // 要读取的第一个数据包的数据包索引。 UInt32 * ioNumPackets, // 输入输出参数 void * outBuffer //输出内存地址 );
- ioNumBytes: 该参数是输入输出参数。也就是说在调用该函数时,需要传入它。在函数执行完成后,该函数会返回输出值。在输入时,表示outBuffer参数的大小(以字节为单位)。在输出时,表示实际读取的字节数。 如果在ioNumPackets参数中请求的数据包数目的字节大小小于在outBuffer参数中传递的缓冲区大小,则输入和输出值将会有所不同。在这种情况下,该参数的输出值小于其输入值。
- outPacketDescriptions: 输出参数,读取数据包的描述数组。您在此参数中传递的数组必须足够大,以适应ioNumPackets参数中请求的数据包数量的描述。该参数仅适用于可变比特率数据。 如果正在读取的文件包含诸如线性PCM的恒定比特率(CBR)数据,则该参数不会被填充。 如果文件的数据格式为CBR,则传递NULL。
- ioNumPackets: 输入输出参数。在输入时,要读取的数据包数。在输出时,实际读取的数据包数。
- outBuffer: 您分配以保存读取数据包的内存。通过将请求的数据包(ioNumPackets参数)乘以文件中音频数据的典型数据包大小来确定适当的大小。对于未压缩的音频格式,数据包等于一个帧。
以上就是本文用到的三个Audio File
相关函数的介绍。下面我们介绍一下 AAC 解码的相关内容。
AAC 解码
AAC 解码与 AAC 编码的逻辑非常类似。
- 首先,设置音频的输入与输出格式。在这里音频的输入格式可以通过上一节中的 AudioFileGetProperty 方法从文件中提取来。
- 其次,创建 AAC 解码器。
- 解码。
设置输出格式
输入格式由通过Audio File
获取。下面是输出格式的代码。如下:
AudioStreamBasicDescription outputFormat; memset(&outputFormat, 0, sizeof(outputFormat)); outputFormat.mSampleRate = 44100; outputFormat.mFormatID = kAudioFormatLinearPCM; outputFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; outputFormat.mChannelsPerFrame = 1; outputFormat.mFramesPerPacket = 1; outputFormat.mBitsPerChannel = 16; outputFormat.mBytesPerFrame = inputFormat.mBitsPerChannel / 8 * inputFormat.mChannelsPerFrame; outputFormat.mBytesPerPacket = inputFormat.mBytesPerFrame * inputFormat.mFramesPerPacket;
创建解码器除了上面说的要设置输入、输出数据格式外,还要告诉 AudioToolbox 是创建编码器还是创建解码器;如果是解码器,还要指定子类型为 lpcm;是硬解码还是软解码。
iOS为我们提供了 AudioClassDescription 来描述这些信息。它包括下面三个字段:
struct AudioClassDescription { OSType mType; OSType mSubType; OSType mManufacturer; };
- mType: 指明提编码器还是解码器。kAudioDecoderComponentType/kAudioEncoderComponentType。
- mSubType: 指明是 lpcm。
- mManufacturer: 指明是软编还是硬编码。
创建解码器
有了上面的输入、输出格式及 AudioClassDescription 信息后,我们就可以创建解码器了。代码如下:
AudioConverterRef audioConverter; memset(&audioConverter, 0, sizeof(audioConverter)); NSAssert(AudioConverterNewSpecific(&inputFramat, & outputFormat, 1, &audioClassDescription, &audioConverter) == 0, nil);
通过上面的代码,编码器就创建好了。下面我们来进行解码。
解码
与编码一样,iOS 使用 AudioConverterFillComplexBuffer 方法进行解码。它的参数如下:
AudioConverterFillComplexBuffer( inAudioConverter: AudioConverterRef, inInputDataProc: AudioConverterComplexInputDataProc, inInputDataProcUserData: UnsafeMutablePointer, ioOutputDataPacketSize: UnsafeMutablePointer<UInt32>, outOutputData: UnsafeMutablePointer<AudioBufferList>, outPacketDescription: AudioStreamPacketDescription ) -> OSStatus
- inAudioConverter : 转码器
- inInputDataProc : 回调函数。用于将AAC数据喂给解码器。
- inInputDataProcUserData : 用户自定义数据指针。
- ioOutputDataPacketSize : 输出数据包大小。
- outOutputData : 输出数据 AudioBufferList 指针。
- outPacketDescription : 输出包描述符。
解码的具体步骤如下:
- 首先,从媒体文件中取出一个音视帧。
- 其次,设置输出地址。
- 然后,调用 AudioConverterFillComplexBuffer 方法,该方法又会调用 inInputDataProc 回调函数,将输入数据拷贝到编码器中。
- 最后,解码。将解码后的数据输出到指定的输出变量中。
具体代码如下所示:
... //从媒体文件中读取一帧数据 OSStatus status = AudioFileReadPacketData( audioFileID, NO, &ioNumBytes, //想要读的io字节数量 audioPacketFormats, //每个包的描述信息数组 idxStartReadPacket, //第一个包的开始位置索引 ioNumberDataPackets,//想要读的包的数量 convertBuffer); //输出地址 ... //设置输入 AudioBufferList inAaudioBufferList; inAaudioBufferList.mBuffers[0].mDataByteSize = ioNumBytes; inAaudioBufferList.mBuffers[0].mData = convertBuffer; //设置输出 uint8_t *buffer = (uint8_t *)malloc(bufferSize); memset(buffer, 0, bufferSize); AudioBufferList outAudioBufferList; outAudioBufferList.mNumberBuffers = 1; outAudioBufferList.mBuffers[0].mNumberChannels = inAaudioBufferList.mBuffers[0].mNumberChannels; outAudioBufferList.mBuffers[0].mDataByteSize = bufferSize; outAudioBufferList.mBuffers[0].mData = buffer; UInt32 ioOutputDataPacketSize = 1; //转码 NSAssert( AudioConverterFillComplexBuffer(audioConverter, inInputDataProc, //在该函数中要将上面的 convertBuffer 数据拷贝到解码器的ioData里。 &inAaudioBufferList, &ioOutputDataPacketSize, &outAudioBufferList, NULL) == 0, nil);
下面我们看一下 inInputDataProc 这个回调函数的具体实现。其中 inUserData
就是在 AudioConverterFillComplexBuffer 方法中传入的第三个参数,也就是输入数据。
inInputDataProc 回调函数的作用就是将输入数据拷贝到 ioData 中。ioData 就是解码器解码时用到的真正输入缓冲区。
OSStatus inInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) { AudioBufferList audioBufferList = *(AudioBufferList *)inUserData; ioData->mBuffers[0].mData = audioBufferList.mBuffers[0].mData; ioData->mBuffers[0].mDataByteSize = audioBufferList.mBuffers[0].mDataByteSize; return noErr; }
至此,AAC解码部分就已经分析完了。下我们再看一下如何将解码后的 PCM 数据播放出来。
播放 PCM
我们使用 iOS 中的 AudioUnit 工具来播放 PCM。AudioUnit的使用步骤如下:
- 设置音频组件描述。其作用是通过该描述信息,可以在iOS中找到相关的音频组件。
- 根据描述查找音视组件。
- 创建 AudioUnit 实例。
- 设置 AudioUnit 属性。
- 播放 PCM。
下面我们来详细介绍下每步:
设置音频描述
// 描述音频元件 AudioComponentDescription desc; desc.componentType = kAudioUnitType_Output; desc.componentSubType = kAudioUnitSubType_RemoteIO; desc.componentFlags = 0; desc.componentFlagsMask = 0; desc.componentManufacturer = kAudioUnitManufacturer_Apple;
该描述信息表明,我们使用AudioUnit的输出组件。
查找音频组件
// 查找一个组件 AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
创建 AudioUnit
OSStatus status; AudioComponentInstance audioUnit; // 获得 Audio Unit status = AudioComponentInstanceNew(inputComponent, &audioUnit); checkStatus(status);
设置属性
#define kOutputBus 0 #define kInputBus 1 ... UInt32 flag = 1; // 为播放打开 IO status = AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &flag, sizeof(flag)); checkStatus(status); // 设置播放格式 status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, & outputFormat, //参见编码器格式 sizeof(audioFormat)); checkStatus(status); // 设置声音输出回调函数。当speaker需要数据时就会调用回调函数去获取数据。它是 "拉" 数据的概念。 callbackStruct.inputProc = playbackCallback; callbackStruct.inputProcRefCon = self; status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, kOutputBus, &callbackStruct, sizeof(callbackStruct)); checkStatus(status);
播放PCM
AudioOutputUnitStart(audioUnit);
小结
本文介绍了如何将一个AAC文件播放出来的步骤。它包括:
- 打开 AAC 媒体文件。
- 获取 AAC 媒体格式。
- 从 AAC 文件中读取一个 AAC 音频帧。
- 通过 AudioToolbox 解决 AAC 到 PCM。
- 通过 AudioUnit 播放 PCM。
- 循环执行 3-5步,直到文件结束。