iOS 音频流播(四)

在第一篇我们介绍过AudioConverter可以对分离出来的音频帧解码得到PCM数据。实际上,AudioConverter可以完成不同线性PCM变体之间音频数据的转换(例如采样位数8位和16位的PCM之间的转换),也可以完成线性PCM和其他压缩格式直接的转换(例如PCM-->MP3)。AudioToolBox将PCM数据作为转换的中间件,所以如果想完成MP3-->AAC的转换,可以先将MP3转码得到PCM,再将PCM转码得到AAC来实现。本篇我们就来说说AudioConverter。

初始化AudioConverter
// 将 inSourceFormat 转换成 inDestinationFormat 格式
extern OSStatus
AudioConverterNew(      const AudioStreamBasicDescription * inSourceFormat,
                        const AudioStreamBasicDescription * inDestinationFormat,
                        AudioConverterRef __nullable * __nonnull outAudioConverter)
  • 第一个参数,inSourceFormat是源输入格式。
  • 第二个参数,inDestinationFormat是目的输出格式。
  • 第三个参数,outAudioConverter是生成的AudioConverter实例,保存留来留作其它方法参数使用。
  • 返回值表示是否成功。
Magic cookie

官方文档这么描述magic cookie:

    In the realm of Core Audio, a magic cookie is an opaque set of 
metadata attached to a compressed sound file or stream. The 
metadata gives a decoder the details it needs to properly decompress 
the file or stream. You treat a magic cookie as a black box, relying 
on Core Audio functions to copy, read, and use the contained metadata.

大意是说magic cookie是附加在音频文件或者音频流中的一组不透明的元数据,而元数据给解码器提供了正确解码音频文件或音频流所必须的细节。我们可以通过Core Audio提供的相关函数读取或使用magic cookie。以下代码片段显示了如何获取和使用magic cookie。

// for AudioFileStream
- (NSData *)fetchMagicCookie
{
    UInt32 cookieSize;
    Boolean writable;
    OSStatus status = AudioFileStreamGetPropertyInfo(_audioFileStreamID, kAudioFileStreamProperty_MagicCookieData, &cookieSize, &writable);
    if (status != noErr)
    {
        return nil;
    }
    
    void *cookieData = malloc(cookieSize);
    status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_MagicCookieData, &cookieSize, cookieData);
    if (status != noErr)
    {
        return nil;
    }
    
    NSData *cookie = [NSData dataWithBytes:cookieData length:cookieSize];
    free(cookieData);
    
    return cookie;
}
// for AudioFile
- (NSData *)fetchMagicCookie
{
    UInt32 cookieSize;
    OSStatus status = AudioFileGetPropertyInfo(_audioFileID, kAudioFilePropertyMagicCookieData, &cookieSize, NULL);
    if (status != noErr)
    {
        return nil;
    }
    
    void *cookieData = malloc(cookieSize);
    status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyMagicCookieData, &cookieSize, cookieData);
    if (status != noErr)
    {
        return nil;
    }
    
    NSData *cookie = [NSData dataWithBytes:cookieData length:cookieSize];
    free(cookieData);
    
    return cookie;
}

获取的magic cookie需要提供给AudioConverter使用。

NSData *cookieData = [self fetchMagicCookie];
AudioConverterSetProperty(_audioConverter, kAudioConverterDecompressionMagicCookie, [cookieData length], [cookieData bytes]);
属性信息

AudioConverter通过getter、setter获取和配置属性。

// 获取属性
extern OSStatus
AudioConverterGetProperty(  AudioConverterRef           inAudioConverter,
                            AudioConverterPropertyID    inPropertyID,
                            UInt32 *                    ioPropertyDataSize,
                            void *                      outPropertyData) 
// 设置属性
extern OSStatus
AudioConverterSetProperty(  AudioConverterRef           inAudioConverter,
                            AudioConverterPropertyID    inPropertyID,
                            UInt32                      inPropertyDataSize,
                            const void *                inPropertyData) 

对于getter:

  • 第一个参数,inAudioConverter是生成的AudioConverter实例。
  • 第二个参数,inPropertyID是需要获取的属性ID。
  • 第三个参数,ioPropertyDataSize是属性对应格式的大小。
  • 第四个参数,outPropertyData是返回的属性值。

对于setter,前两个参数同上,区别在于第三个、第四个参数是要设置的属性大小和对应的值。

以下是AudioConverter定义的属性ID。

CF_ENUM(AudioConverterPropertyID)
{
    kAudioConverterPropertyMinimumInputBufferSize       = 'mibs',
    kAudioConverterPropertyMinimumOutputBufferSize      = 'mobs',
    kAudioConverterPropertyMaximumInputBufferSize       = 'xibs',
    kAudioConverterPropertyMaximumInputPacketSize       = 'xips',
    kAudioConverterPropertyMaximumOutputPacketSize      = 'xops',
    kAudioConverterPropertyCalculateInputBufferSize     = 'cibs',
    kAudioConverterPropertyCalculateOutputBufferSize    = 'cobs',
    kAudioConverterPropertyInputCodecParameters         = 'icdp',
    kAudioConverterPropertyOutputCodecParameters        = 'ocdp',
    kAudioConverterSampleRateConverterAlgorithm         = 'srci',
    kAudioConverterSampleRateConverterComplexity        = 'srca',
    kAudioConverterSampleRateConverterQuality           = 'srcq',
    kAudioConverterSampleRateConverterInitialPhase      = 'srcp',
    kAudioConverterCodecQuality                         = 'cdqu',
    kAudioConverterPrimeMethod                          = 'prmm',
    kAudioConverterPrimeInfo                            = 'prim',
    kAudioConverterChannelMap                           = 'chmp',
    kAudioConverterDecompressionMagicCookie             = 'dmgc',
    kAudioConverterCompressionMagicCookie               = 'cmgc',
    kAudioConverterEncodeBitRate                        = 'brat',
    kAudioConverterEncodeAdjustableSampleRate           = 'ajsr',
    kAudioConverterInputChannelLayout                   = 'icl ',
    kAudioConverterOutputChannelLayout                  = 'ocl ',
    kAudioConverterApplicableEncodeBitRates             = 'aebr',
    kAudioConverterAvailableEncodeBitRates              = 'vebr',
    kAudioConverterApplicableEncodeSampleRates          = 'aesr',
    kAudioConverterAvailableEncodeSampleRates           = 'vesr',
    kAudioConverterAvailableEncodeChannelLayoutTags     = 'aecl',
    kAudioConverterCurrentOutputStreamDescription       = 'acod',
    kAudioConverterCurrentInputStreamDescription        = 'acid',
    kAudioConverterPropertySettings                     = 'acps',
    kAudioConverterPropertyBitDepthHint                 = 'acbd',
    kAudioConverterPropertyFormatList                   = 'flst'
};

茫茫多~ 讲几个比较重要的属性吧。

  • kAudioConverterDecompressionMagicCookie:magic cookie相关,上面已经提到了。注意一下magic cookie不是一定有的,所有要先获取一下,如果有就设置给AudioConverter。
  • kAudioConverterCurrentInputStreamDescription 和
    kAudioConverterCurrentOutputStreamDescription:我们知道在AudioConverter初始化的时候需要传递srcFormat和dstFormat。这两个format并不一定已经被完全填写。比如我们要转码成AAC,dstFormat就不会被完全填写。可以通过AudioConverterGetProperty()获取一下,大概是这样:
// 初始化时传递的源格式
AudioStreamBasicDescription srcFormat;
// 初始化时传递的目标格式
AudioStreamBasicDescription dstFormat;
// 获取对应的真实可用格式
 UInt32 size = sizeof(srcFormat);
OSStatus status =  AudioConverterGetProperty(converter, kAudioConverterCurrentInputStreamDescription, &size, &srcFormat);
if (status == noErr)
{
    // 错误处理
}
size = sizeof(dstFormat);
status = AudioConverterGetProperty(converter, kAudioConverterCurrentOutputStreamDescription, &size, &dstFormat)
if (status == noErr)
{
    // 错误处理
}
// 接下来 srcFormat 和  dstFormat就已经填充完整。
// ...
  • kAudioConverterPropertyMaximumOutputPacketSize:如果目标格式是VBR类型,获取此属性的值用来计算输出的AudioStreamBasicDescription数组的大小,从而分配合适的内存。因为VBR是不定的,但是如果分配最大音频包大小的内存,就可以hold住所有的了。
    if (dstFormat.mBytesPerPacket == 0) {
        // if the destination format is VBR,
       //  we need to get max size per packet from the converter
        size = sizeof(dstFormat.mBytesPerPacket);
       OSStatus status = AudioConverterGetProperty(converter, kAudioConverterPropertyMaximumOutputPacketSize, &size, &dstFormat.mBytesPerPacket) ; 
        // allocate memory for the PacketDescription structures 
        // describing the layout of each packet
        AudioStreamPacketDescription *outputPacketDescriptions = calloc(theOutputBufferSize / dstFormat.mBytesPerPacket, sizeof(AudioStreamPacketDescription));
    }
  • kAudioConverterPrimeInfo:AudioConverter的启动信息。一些音频数据格式转换,特别是那些涉及采样率转换的音频数据格式转换,当有leadingFrames或trailingFrames可用时,会产生更高质量的输出。 这些启动信息的适当数量取决于输入的音频数据格式。
// 对于一些音频数据,它的数据并不全是有效可播放的
// 可能存在一些启动信息,也可能有一些剩余帧被添加到完整的音频包中
// 
// struct AudioConverterPrimeInfo {
      // 前导帧
//    UInt32      leadingFrames;
      // 尾帧
//    UInt32      trailingFrames;
// };
// typedef struct AudioConverterPrimeInfo AudioConverterPrimeInfo;
// 
// struct AudioFilePacketTableInfo
// {
           // 有效的音频帧数
//         SInt64  mNumberValidFrames;
          //  相当于leadingFrames
//         SInt32  mPrimingFrames;
          //  相当于trailingFrames
//         SInt32  mRemainderFrames;
// };
// typedef struct AudioFilePacketTableInfo AudioFilePacketTableInfo;
//
// 在转码成PCM时,我们可以获取AudioFilePacketTableInfo,用它来填充AudioConverterPrimeInfo
    if (srcFormat.mBitsPerChannel == 0) { // VBR
        // 获取PacketTableInfo
        size = sizeof(srcPti);
        status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyPacketTableInfo, &size, &srcPti);
        if (status == noErr) {
            // 先确保可写
            UInt32 dataSize = 0;
            Boolean isWritable = NO;
            status = AudioConverterGetPropertyInfo(_audioConverter, kAudioConverterPrimeInfo, &dataSize, &isWritable);
            if (status == noErr && isWritable) {
                // 设置AudioConverter启动信息
                // 填充启动信息,可以获得可高质量的输出
                AudioConverterPrimeInfo primeInfo;
                primeInfo.leadingFrames = (UInt32)(srcPti.mPrimingFrames * actualToBaseSampleRateRatio);
                primeInfo.trailingFrames = (UInt32)srcPti.mRemainderFrames * actualToBaseSampleRateRatio;
                status = AudioConverterSetProperty(_audioConverter, kAudioConverterPrimeInfo, sizeof(primeInfo), &primeInfo);
                if (status != noErr) {
                    return NO;
                }
            }
        }
    }

  这里解释一下actualToBaseSampleRateRatio。前文说过AudioFile的两个propertyID,kAudioFilePropertyFormatList和kAudioFilePropertyDataFormat,这两个属性都可以获取到AudioStreamBasicDescription,区别在于 kAudioFilePropertyDataFormat只能获取到最低级别的编码层级。
  例如对源文件采用AAC_HE_V2编码格式,44100KHz采样率、双声道:

  • 第一层:只支持22050,单声道
  • 第二层,支持44100,单声道
  • 第三层支持44100,双声道。

  在这种情况下,用kAudioFilePropertyDataFormat就取不到第三层的格式了,这时,就需要用kAudioFilePropertyFormatList来获取第三层的格式。姑且将kAudioFilePropertyFormatList获取的format称为高规格,kAudioFilePropertyDataFormat获取的format称为低规格。actualToBaseSampleRateRatio就是高规格的format对低规格format的采样率的比率。第一篇我们讲过采样率对音频文件大小的影响,所以剩下的就靠想象了。

编解码

AudioConverter提供了三个函数用于编解码。

OSStatus AudioConverterConvertBuffer(AudioConverterRef inAudioConverter, 
                                               UInt32 inInputDataSize, 
                                               const void *inInputData,
                                               UInt32 *ioOutputDataSize, 
                                               void *outOutputData);
OSStatus AudioConverterConvertComplexBuffer( AudioConverterRef               inAudioConverter,
                                    UInt32                          inNumberPCMFrames,
                                    const AudioBufferList *         inInputData,
                                    AudioBufferList *               outOutputData)

这两个函数功能类似,都只支持PCM之间的转换,并且两种PCM的采样率必须一致。也就是说无法从PCM转换成其他压缩格式或者从压缩格式转换成PCM,下面重点介绍另一个函数,AudioConverterFillComplexBuffer()。

extern OSStatus
AudioConverterFillComplexBuffer(    AudioConverterRef                   inAudioConverter,
                                    AudioConverterComplexInputDataProc  inInputDataProc,
                                    void * __nullable                   inInputDataProcUserData,
                                    UInt32 *                            ioOutputDataPacketSize,
                                    AudioBufferList *                   outOutputData,
                                    AudioStreamPacketDescription * __nullable outPacketDescription)
  • 第一个参数,inAudioConverter是初始化得到的AudioConverter对象。
  • 第二个参数,inInputDataProc是提供音频数据进行转换的回调函数。当AudioConverter准备好新的输入数据时,这个回调被重复调用。
  • 第三个参数,inInputDataProcUserData是上下文对象。
  • 第四个参数,ioOutputDataPacketSize,在输入时代表另一个参数outOutputData的大小(以音频包表示),在输出时会写入已经转换了的数据包数。如果调用完毕ioOutputDataPacketSize == 0,说明EOF(end of file)。
  • 第五个参数,outOutputData代表转换后的数据输出。
  • 第六个参数,outPacketDescription在输入时,必须指向能够保存ioOutputDataPacketSize * sizeof(AudioStreamPacketDescription)内存块。在输出时如果非空,并且AudioConverter的输出格式使用AudioStreamPacketDescription来描述,则会被写入一个AudioStreamPacketDescription数组。

来看一下为AudioConverter提供输入的回调

typedef OSStatus
(*AudioConverterComplexInputDataProc)(  AudioConverterRef               inAudioConverter,
                                        UInt32 *                        ioNumberDataPackets,
                                        AudioBufferList *               ioData,
                                        AudioStreamPacketDescription * __nullable * __nullable outDataPacketDescription,
                                        void * __nullable               inUserData);
  • 第一个参数不用多说。
  • 第二个参数,ioNumberDataPackets在输入时,代表AudioConverter可以完成本次转换所需要的最小数据包数,在输出时,代表实际转换的音频数据包数。
  • 第三个参数,ioData在输出时,将此结构体的字段指向要提供的要转换的音频数据。
  • 第四个参数,在输入时,如果不为NULL,则需要在输出时提供一组AudioStreamPacketDescription结构,用于给ioData参数中提供AudioStreamPacketDescription描述信息。

具体使用例子可以看这里;

清理

AudioConverter使用完毕后需要清理。

OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容