在第一篇我们介绍过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);