iOS下解码AAC并播放

作者: 音视频直播技术专家 | 来源:发表于2017-10-17 22:17 被阅读820次

前言

今天我们介绍一下如何在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 可以用来创建、初始化音频文件;读写音频数据;对音频文件进行优化;读取和写入音频格式信息等等,功能十分强大。

我们看一下用到的几个函数原型及其参数说明。

  1. AudioFileOpenURL用于打开一个媒体文件。原型如下:

    enum {
      kAudioFileReadPermission      = 0x01,
      kAudioFileWritePermission     = 0x02,
      kAudioFileReadWritePermission = 0x03
    };
    
    extern OSStatus AudioFileOpenURL (
        CFURLRef inFileRef, // 打开文件的路径
        SInt8 inPermissions, // 打开文件的权限。 读/写/读写三种权限
        AudioFileTypeID inFileTypeHint, // 文件类型提示信息,如果明确知道就填入,如果不知道填0.
        AudioFileID * outAudioFile // 文件述符 ID
    ); 
    
  2. 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 //输出值地址。
    );
    
  3. 从媒体文件中读取一帧数据

    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步,直到文件结束。

希望本文能对您有所帮助。并请多多关注。谢谢!

相关文章

网友评论

  • 哎呦YY:看了几个您的文章,但是并没有我遇到的情况,https://stackoverflow.com/questions/36464766/ios-playing-rtp-packets-using-audio-unit类似这个的B方向:
    我现在到了渲染这一步,有个buffer装上层过来的PCM,audiounit回调自己来取数据,我看audiounit周期均匀,但是有一个这样的现象:我把buffer开的30K左右,就会明显感觉渲染后一段时间音视频后行都对不上;吧buffer开小点,6K左右,就会出现频繁丢数据,现在已经到了可以感觉到的地步,测试反馈了该问题,但是我无从下手,这种情况出现在实时流模式播放的时候,文件模式播放就不会出现buffer满的情况,也不会有不同步;总结还是流式播放不稳定,但是问题是需要解决的,希望前辈给与指导.是否遇到过类似问题以及是有有什么audiounit时钟可以控制buffer满的时候快放,让他不至于明显听到声音 "脏脏的"
    音视频直播技术专家:@哎呦YY 那个没办法,音频的采样率就是那个样子,时间也就固定了。为一的办法就是丢数据。丢的时候可以把整个缓冲区全部丢掉
    哎呦YY:@音视频直播技术专家 是的,上层已经做了同步,但是到下层渲染层实时流就是会堆积数据,满了我就丢,这也没办法,毕竟实时性是第一位的,但是这样丢的话,底层是会听得出来的的(尽管不是很明显,但是感受不太清晰,干净) 有没有什么可以控制底层音频渲染回调周期的,让他在满了的时候快一点
    音视频直播技术专家:@哎呦YY 这个要从网络上解决,也就是如果网络不稳定的情况下,要通过buffer来缓存网络数据。对于音频的播放来说,它是按时来取的,如果此时没有数据,直接播静音就好了。音视频之前的同频是在上层处理。而不是放到播放的阶段。
  • 63a49d179b94:请问,我用AudioConverterFillComplexBuffer这个函数解码AAC时,函数的返回值是561015652,解码失败,这是为什么呀?编码是成功的
  • 8ab85fad1fa1:AAC的解码 有示例的Demo么?
    8ab85fad1fa1:@音视频直播技术专家 AudioClassDescription该怎么设置呢?看了其他例子 都获取不到
    8ab85fad1fa1:@音视频直播技术专家 :+1: :+1:
    音视频直播技术专家:可以看一下webrtc的例子,或者在 github 上找一下相关例子,这类例子还是很多的。
  • 8f64fc6e6524:如果有示例代码就更好了,新手看起来好懵逼:blush:

本文标题:iOS下解码AAC并播放

本文链接:https://www.haomeiwen.com/subject/adyjuxtx.html