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