今天我们介绍一下如何在iOS进行AAC解码,并使用AudioUnit播放解码后的PCM数据。
iOS系统对音频处理做了三层封装。包括应用层、服务层和硬件层。如下图所示:
我们本次使用的都是服务层的接口。也就是上图中被红色框起来的部分。该层更接近于底层,所以灵活性更大,性能也更好。尤其对于直播相关的项目最好使用该层接口。
在iOS下进行音频解码及播放的大体流程如下:
下面我们对以上每一步做详细介绍。
上面流程中第1、2、3步使用Audio File
服务。Audio File
可以用来创建、初始化音频文件;读写音频数据;对音频文件进行优化;读取和写入音频格式信息等等,功能十分强大。
我们看一下用到的几个函数原型及其参数说明。
以上就是本文用到的三个Audio File
相关函数的介绍。下面我们介绍一下 AAC 解码的相关内容。
AAC 解码与 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;
};
有了上面的输入、输出格式及 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
解码的具体步骤如下:
具体代码如下所示:
...
//从媒体文件中读取一帧数据
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 数据播放出来。
我们使用 iOS 中的 AudioUnit 工具来播放 PCM。AudioUnit的使用步骤如下:
下面我们来详细介绍下每步:
// 描述音频元件
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);
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);
AudioOutputUnitStart(audioUnit);
本文介绍了如何将一个AAC文件播放出来的步骤。它包括: