今天记录是的是 使用
AudioToolbox
框架 使用
AudioConverterRef
工具进行本地音频文件的编码和解码。
本文打仓库代码为:
JBLocalAudioFileConvecter
分别实现了:
-
flac
,
mp3
等其他音频编码文件 转换成
pcm
文件。 (解码) -
pcm
文件 转换成
flac
,
mp3
等其他音频编码文件。 (编码)
两者的代码基本一样,只是在输入和输出的时候对应的 静态码率(CBR)和动态码率(VBR),在处理和取值的时候有所区别。
运行本demo对应的ui 界面的按钮入口为:
在这里插入图片描述
1. 全局变量
列出来头部 所申明的所有全局属性和成员变量。都有说明。
@interface JBLocalAudioFileConvecter()
{
@public
AudioFileID _inFile; //输入文件指针
AudioFileID _outFile; //输出文件1 .caf 文件会在头部包含必要的asbd信息
FILE *_outFile_2; //输出文件2 .pcm全是裸数据
char * _inBuffer; //复用的输入缓冲区
}
@property (atomic, assign) BOOL isRunning; //代表是否正在编解码
@property (nonatomic, strong) dispatch_queue_t workQueue;
@property (nonatomic, strong) NSURL *inFileURL;
@property (nonatomic, strong) NSURL *outFileURL1; //输出文件 使用 AudioFileID
@property (nonatomic, strong) NSURL *outFileURL2; //输出文件 使用 FILE *
/**------ 输入 ---------**/
@property (nonatomic, assign) AudioStreamBasicDescription inASBD;
@property (nonatomic, assign) UInt32 inMaxSizePerPacket; //输入文件包里面的最大尺寸的包的尺寸
@property (nonatomic, assign) UInt32 inNumPacketPerRead; //输入文件的,自己malloc的buffer内能容纳的最大packet数量
@property (nonatomic, assign) UInt32 inPacketReadIndex; //输入文件读取的包的下标
@property (nonatomic, assign) AudioStreamPacketDescription *inASPDs;
/**------ 输出 ---------**/
@property (atomic, assign) AudioFormatID outputFormat; //输出文件格式,pcm? aac? flac? mp3?
@property (nonatomic, assign) AudioStreamBasicDescription outASBD;
@property (nonatomic, assign) AudioStreamPacketDescription *outASPDs;
@end
2. 入口函数
这里输入一个文件,然后输出了两种文件,两种输出文件的写入方式稍微有点区别。
并且 工作的显示是在
workQueue
的子线程中,不阻塞主线程。
flac -> pcm 解码入口
outputFormat
设置为
kAudioFormatLinearPCM
, 代表着后面会 已
PCM
进行输出。
- (void)startConvertFlac_to_pcm {
if (self.isRunning) {
return;
};
[self setupStartData];
dispatch_async(self.workQueue, ^{
self.inFileURL = [[NSBundle mainBundle] URLForResource:@"句号_10s" withExtension:@"flac"];
self.outFileURL1 = [JBHelper getOutputPathWithFile:@"output.caf"]; //包含头信息
self.outFileURL2 = [JBHelper getOutputPathWithFile:@"pcm_output.pcm"]; //纯裸数据
self.outputFormat = kAudioFormatLinearPCM;
[self workSubThread];
});
}
pcm -> flac 编码入口
outputFormat
设置为
kAudioFormatFLAC
, 代表着后面会 以
FLAC
格式进行输出
- (void)startConvertPcm_to_flac {
if (self.isRunning) {
return;
};
[self setupStartData];
dispatch_async(self.workQueue, ^{
self.inFileURL = [[NSBundle mainBundle] URLForResource:@"pcm_44100_setro_s16be" withExtension:@"caf"];
self.outFileURL1 = [JBHelper getOutputPathWithFile:@"apple_out.flac"];
self.outFileURL2 = [JBHelper getOutputPathWithFile:@"c_out.flac"];
self.outputFormat = kAudioFormatFLAC; //可以改成其他类型,比如aac,MP3等
[self workSubThread];
});
}
其中两者都调用了,
setupStartData
函数,它是重置所有变量的。
- (void)setupStartData {
self.isRunning = YES;
self.inASPDs = NULL;
self.outASPDs = NULL;
self.inASPDs = NULL;
_outFile_2 = NULL;
_inBuffer = NULL;
_inFile = NULL;
_outFile = NULL;
self.outputFormat = kAudioFormatLinearPCM;
self.inPacketReadIndex = 0;
}
3. 首先打开 输入音频文件
工作函数(子线程)
workSubThread
是一个很长的函数,基本操作都在这里,之所以没抽多少出去,是因为看流程的时候比较方便。
首先打开 输入音频文件
JBAssertNoError(AudioFileOpenURL((__bridge CFURLRef)self.inFileURL, kAudioFileReadPermission, 0, &_inFile), @"AudioFileOpenURL");
这里开始有个宏
JBAssertNoError
, 需要说下是定义在
JBHelper.h
中的,会判断这行代码的返回
status
是否有问题,出错了就 进入断言,停止程序。
#define JBAssertNoError(inError, inMessage) \
{ \
SInt32 __Err = (inError); \
if(__Err != 0) \
{ \
NSLog(@"==== 出现错误: %@ code: %d(%s)", inMessage, __Err, FourCC2Str(__Err));\
NSAssert(__Err == 0, inMessage);\
}\
}
4. 完善输入输出的ASBD
4.1 获取输入文件的ASBD
设置到全局变量
_inASBD
里
UInt32 size = sizeof(AudioStreamBasicDescription);
JBAssertNoError(AudioFileGetProperty(_inFile, kAudioFilePropertyDataFormat, &size, &_inASBD), @"AudioFileGetProperty kAudioFilePropertyDataFormat");
4.2 填充输出文件的ASBD
-
这儿是根据输入文件的
ASBD
和前面设置的输出的文件格式
self.outputFormat
综合生成的 -
需要注意的是,如果是非
PCM
的话,大部分的
ASBD
的值都需要使用
AudioFormatGetProperty
函数去自动获取,因为有些格式是
VBR
,数据是不定的,不能手动填充
//填充了一个 16字节的 整型的,音频数据格式
- (void)fillUpOutASBDWithInputFile {
AudioStreamBasicDescription asbd = {0};
//mSampleRate 使用 输入文件的 的值
asbd.mSampleRate = self.inASBD.mSampleRate;
//输入固定为pcm文件
asbd.mFormatID = self.outputFormat;
if (asbd.mFormatID == kAudioFormatLinearPCM) {
//kAudioFormatFlagIsSignedInteger 还是 kAudioFormatFlagIsFloat 看自己输入而定
//大端,小端,根据自己情况而定
asbd.mFormatFlags = kAudioFormatFlagIsBigEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
//下面的数据都是计算得出
asbd.mChannelsPerFrame = self.inASBD.mChannelsPerFrame;
asbd.mBitsPerChannel = 16;
asbd.mBytesPerFrame = (asbd.mBitsPerChannel >> 3) * asbd.mChannelsPerFrame;
//pcm的一个包里面只有一个小样本帧
asbd.mFramesPerPacket = 1;
asbd.mBytesPerPacket = asbd.mFramesPerPacket * asbd.mBytesPerFrame;
asbd.mReserved = self.inASBD.mReserved;
} else {
//非pcm的话,其他参数可能设置不正确,需要调用api来自动赋值。
asbd.mChannelsPerFrame = (asbd.mFormatID == kAudioFormatiLBC ? 1 : self.inASBD.mChannelsPerFrame);
UInt32 size = sizeof(asbd);
//使用format api 自动填充对应的参数数据
JBAssertNoError(AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &size, &asbd), @"AudioFormatGetProperty kAudioFormatProperty_FormatInfo");
}
self.outASBD = asbd;
}
5. 创建和打开输入文件
分别创建了两个输出文件,进行测试对比
//打开 输出 音频 文件1
JBAssertNoError(AudioFileCreateWithURL((__bridge CFURLRef)self.outFileURL1, kAudioFileCAFType, &_outASBD, kAudioFileFlags_EraseFile, &_outFile),@"AudioFileCreateWithURL");
//打开 输出 音频 文件2
if (_outFile_2 == NULL) {
NSString *pathStr_cfile = [self.outFileURL2.absoluteString stringByReplacingOccurrencesOfString:@"file://" withString:@""];
_outFile_2 = fopen([pathStr_cfile UTF8String], "wb++");
}
6. 创建AudioConverterRef 转换对象
根据输入和输出的
ASBD
进行创建绑定
//创建转换对象
AudioConverterRef audioConverter;
JBAssertNoError(AudioConverterNew(&_inASBD, &_outASBD, &audioConverter),@"AudioConverterNew");
7. 读取和设置magic cookie
magic cookie
的简单作用就是 存储
metadata
和部分编解码所需要的数据。
我们可以看下下图,
flac
转
pcm
解码出来的文件里面的
magic cookie
的前面是啥样的, 可以看到
16
进制转换后,有
caff
类型,里面包含的格式是
lpcm
的格式。
- 从输入文件中读取
-
设置到
audioConver
中 -
后面会在
第11 节
,设置到 输出文件中
- (void)readMagicCookie:(AudioConverterRef)converter {
//先获取长度
UInt32 cookieDataSize = 0;
UInt32 isWriteAble = 0;
//注意这里是AudioFileGetPropertyInfo, 获取长度和是否可以写
OSStatus status = AudioFileGetPropertyInfo(_inFile, kAudioFilePropertyMagicCookieData, &cookieDataSize, &isWriteAble);
//有些没有 magic cookie ,所以不管
if (status != noErr) {
NSLog(@"读取 magic cookie 不存在,忽略掉");
return;
}
if (cookieDataSize <= 0) {
NSLog(@"AudioFileGetPropertyInfo kAudioFilePropertyMagicCookieData get zero size data");
return;
}
//根据长度获取对应的magic data 的内容
Byte *cookieData = malloc(cookieDataSize *sizeof(Byte));
//这里是AudioFileGetProperty
JBAssertNoError(AudioFileGetProperty(_inFile, kAudioFilePropertyMagicCookieData, &cookieDataSize, cookieData),@"AudioFileGetProperty kAudioFilePropertyMagicCookieData");
//将获取的MagicCookie 设置到 converter 中
JBAssertNoError(AudioConverterSetProperty(converter,
kAudioConverterDecompressionMagicCookie,
cookieDataSize,
cookieData),
@"AudioConverterSetProperty kAudioConverterDecompressionMagicCookie");
// malloc 后必须 free
free(cookieData);
}
8. 重置获取校验过的ASBD
ASBD 设置到 AudioConverter 后,被在里面被自动修正某些有问题的值, 所以我们重新从里面获取修正后的值
//重新从 Audio Convert 获取被校正过的 ASBD数据
JBAssertNoError(AudioConverterGetProperty(audioConverter, kAudioConverterCurrentInputStreamDescription, &size, &_inASBD), @"kAudioConverterCurrentInputStreamDescription get infile");
JBAssertNoError(AudioConverterGetProperty(audioConverter, kAudioConverterCurrentOutputStreamDescription, &size, &_outASBD), @"kAudioConverterCurrentInputStreamDescription get outfile");
9. 开辟输入缓冲区
大体流程如下
-
创建
_inBuffer
全局复用,大小手动设定的 -
VBR
的话,需要获取 每次读取的
packet
的数量,根据数量创建 输入的
aspd
的内存大小,供后面解码使用。 CBR的话就不需要。
//设置输入缓冲区的数据大小
UInt32 inBufferSize = 4096*8;
_inBuffer = malloc(inBufferSize);
if (self.inASBD.mBytesPerPacket == 0) {
//输入文件的 单个packet 理论上的最大大小。
/**
据了解 kAudioFilePropertyPacketSizeUpperBound 不会打开整个文件,只是预测
kAudioFilePropertyMaximumPacketSize 有可能会打开文件来计算
*/
UInt32 inSizePerPacket = 0; //18466 in this file aac
size = sizeof(inSizePerPacket);
JBAssertNoError(AudioFileGetProperty(_inFile, kAudioFilePropertyPacketSizeUpperBound, &size, &inSizePerPacket), @"kAudioFilePropertyPacketSizeUpperBound");
self.inMaxSizePerPacket = inSizePerPacket;
//每次读取的packet的数量,必须根据我们 输入缓冲区的大小,来决定
self.inNumPacketPerRead = inBufferSize / inSizePerPacket;
// VBR 可变比特率
self.inASPDs = calloc(self.inNumPacketPerRead, sizeof(AudioStreamPacketDescription));
} else {
// CBR 固定比特率
self.inMaxSizePerPacket = self.inASBD.mBytesPerPacket;
self.inNumPacketPerRead = inBufferSize / self.inASBD.mBytesPerPacket;
self.inASPDs = NULL;
}
10. 开辟输出缓冲区
这里输出缓冲区的大小和输入一样,当然也可以设置成不一样的值。
如果 输出是
VBR
的话,也需要获取每次输出 的
包
的数量,根据数量创建 输出的
aspd
的内存大小,供后面编码使用。
CBR
的话就不需要。
/** 配置输出信息 */
char *outBuffer;
UInt32 outBufferSize = 4096*8; //输出缓冲区的大小,这里设置成和输入一样的值
outBuffer = (char *)malloc(outBufferSize);
memset(outBuffer, 0, outBufferSize);
UInt32 outSizePerPacket = self.outASBD.mBytesPerPacket; //4
//理论上输入缓冲区能容纳下的最大 packet 数量
UInt numPaketPerOut = outBufferSize / outSizePerPacket;
if(outSizePerPacket == 0) {
//输出的 VBR, 需要重新计算每个包的 包大小
getAudioConverterProperty(audioConverter,
kAudioConverterPropertyMaximumOutputPacketSize,
&outSizePerPacket,
@"kAudioConverterPropertyMaximumOutputPacketSize");
numPaketPerOut = outBufferSize / outSizePerPacket;
self.outASPDs = calloc(numPaketPerOut, sizeof(AudioStreamPacketDescription));
}
11. 写入magic cookie
写入
第7节
设置的值
- (void)writeMagicCookie:(AudioConverterRef)converter {
UInt32 cookieDataSize = 0;
OSStatus status = AudioConverterGetPropertyInfo(converter, kAudioConverterCompressionMagicCookie, &cookieDataSize, NULL);
if (status != noErr && cookieDataSize == 0) {
NSLog(@"写入 magic cookie 不存在,忽略掉");
return;
}
void *cookies = malloc(cookieDataSize);
status = AudioConverterGetProperty(converter, kAudioConverterDecompressionMagicCookie, &cookieDataSize, cookies);
if (status == noErr) {
status = AudioFileSetProperty(_outFile, kAudioFilePropertyMagicCookieData, cookieDataSize, cookies);
printErr(@"AudioFileSetProperty kAudioFilePropertyMagicCookieData", status);
} else {
NSLog(@"magic cookie 是空的,忽略");
}
free(cookies);
}
12. while 循环进行 数据处理
这里就是 最重要的 读取文件,编解码,写入文件的环节了。
一个while 循环进行处理数据,当读完了,或者出错了,才退出循环。
-
首先每次创建
outBufferList
的栈变量供输出数据使用,其中里面的
data
为 前面开辟的
outBuffer
复用内存,大小也是前面算出来的。 -
AudioConverterFillComplexBuffer
调用这个函数,进行音频转换填充,其中第二个参数为函数指针(在 下一节讲里面的代码),会在里面进行读取数据,并塞入上一步创建debuffer 中,系统自动进行数据转换,转换完成后,这个函数会有个返回值。所以说这个函数是阻塞式的。 -
至此 数据转换完成,并放入了
outBuffer
,也就是
outBufferList.mBuffers[0].mData
中,就是我们编解码好的数据,我们将 调用
AudioFileWritePackets
和
fwrite
分别写入 输出文件1 和 2 中。两者写入时候传的参数有所区别。 -
最后不要完了,
outFilePacketOffset += ioOutDataPacketsPerOut;
, 不然下次写入的位置不对
UInt64 totalOutputFrames_debug = 0;
UInt32 outFilePacketOffset = 0; //输出文件的写入偏移量
OSStatus status = noErr;
//阻塞式
while (true) {
if (!_isRunning) {
break;
}
//创建输出的AudioBuffer
AudioBufferList outBufferList = {0};
outBufferList.mNumberBuffers = 1;
outBufferList.mBuffers[0].mNumberChannels = self.outASBD.mChannelsPerFrame;
outBufferList.mBuffers[0].mDataByteSize = outBufferSize;
outBufferList.mBuffers[0].mData = outBuffer; //这里直接将我们上面malloc的对象赋值进去,每次重用堆空间
//malloc的输入缓冲区能够装下的最大包数量
UInt32 ioOutDataPacketsPerOut = numPaketPerOut;
status = AudioConverterFillComplexBuffer(audioConverter,
JBAudioConverterCallback,
(__bridge void *)self,
&ioOutDataPacketsPerOut, //输出的数量
&outBufferList, //输出的buffer
self.outASPDs //输出文件的aspd,我们在上面开辟了内存
);
printErr(@"AudioConverterFillComplexBuffer", status);
if(status != noErr) {
NSLog(@"AudioConverterFillComplexBuffer--- 失败了,退出");
break;
} else if (ioOutDataPacketsPerOut == 0) {
//EOF, 文件读完了
status = noErr;
break;
}
NSLog(@"convert 输出:写入包数量:%d, offset:%d size:%d,", ioOutDataPacketsPerOut, outFilePacketOffset, outBufferList.mBuffers[0].mDataByteSize);
//写入文件 到 AudioFile
JBAssertNoError(AudioFileWritePackets(_outFile,
false,
outBufferList.mBuffers[0].mDataByteSize,
self.outASPDs,
outFilePacketOffset,
&ioOutDataPacketsPerOut,
outBuffer),
@"AudioFileWritePackets");
//写入文件到FILE *
fwrite((char *)outBufferList.mBuffers[0].mData, sizeof(char), outBufferList.mBuffers[0].mDataByteSize, _outFile_2);
//debug 统计
if (self.outASBD.mBytesPerPacket > 0) {
totalOutputFrames_debug += (ioOutDataPacketsPerOut * self.outASBD.mFramesPerPacket);
} else if (self.outASPDs) {
for(UInt32 i= 0; i < ioOutDataPacketsPerOut; i++) {
totalOutputFrames_debug += self.outASPDs[i].mVariableFramesInPacket;
}
}
//下次输出文件的时候,需要增加这次输出的数量
outFilePacketOffset += ioOutDataPacketsPerOut;
} //end while
13. 音频转换的函数指针
在上一步
AudioConverterFillComplexBuffer
中,我们配置了这函数,系统会在特定时候调用这个函数,我们只需要读取特定大小的数据,写入
ioData
中,也就是上一步的
outBufferList
中。
-
首先我们需要
ioNumberDataPackets
判断,读取的数量是否 会超出我们开辟的空间大小 -
AudioFileReadPacketData
然后调用函数进行读取文件,并输出到
_inBuffer
这个全局变量中, -
jbClass.inPacketReadIndex
输入文件的偏移量往后移 -
ioData
根据自己实际获取的数据,填充这个对象。 -
如果输入文件时VBR的话,也需要填充
outDataPacketDescription
, 方便进行解码和计算
最后系统会自动对
ioData
进行编解码数据转换,并在上一节的
outBufferList
对象里面获取 转换好的值。
static OSStatus JBAudioConverterCallback (AudioConverterRef inAudioConverter,
UInt32 * ioNumberDataPackets,
AudioBufferList * ioData,
AudioStreamPacketDescription * __nullable * __nullable outDataPacketDescription,
void * __nullable inUserData) {
JBLocalAudioFileConvecter *jbClass = (__bridge JBLocalAudioFileConvecter *)inUserData;
//不能超过我们开的的内存,的最大空间
UInt32 maxPackets = jbClass.inNumPacketPerRead;
if(*ioNumberDataPackets > maxPackets) {
*ioNumberDataPackets = maxPackets;
}
if (*ioNumberDataPackets <= 0) {
NSLog(@"*ioNumberDataPackets <= 0");
//读完了,没有了
return noErr;
}
//读取文件到 内存
UInt32 outNumBytes = jbClass.inMaxSizePerPacket * (*ioNumberDataPackets); //这个值必须非0,根据最大包大小计算的值,读取的时候会改成实际值
OSStatus status = AudioFileReadPacketData(jbClass->_inFile,
false,
&outNumBytes,
jbClass.inASPDs,
jbClass.inPacketReadIndex,
ioNumberDataPackets,
jbClass->_inBuffer);
printErr(@"AudioFileReadPacketData", status);
NSLog(@"io读取:%d", outNumBytes);
if (eofErr == status) return noErr;
jbClass.inPacketReadIndex += *ioNumberDataPackets;
//将自己获取的到buffer,塞入 io队列中。
ioData->mBuffers[0].mData = jbClass->_inBuffer;
ioData->mBuffers[0].mDataByteSize = outNumBytes;
ioData->mBuffers[0].mNumberChannels = jbClass.inASBD.mChannelsPerFrame;
//aspd
if (outDataPacketDescription) {
*outDataPacketDescription = jbClass.inASPDs;
}
return noErr;
}
14. 处理后续需要写入的尾巴数据
while 循环结束,转换结束的时候,可能还有些末尾的meta数据需要进行追加。
if (status == noErr) {
if (self.outASBD.mBitsPerChannel == 0) {
NSLog(@"总共写入frame数量: %lld", totalOutputFrames_debug);
[self writePacketTableInfo_inTrailer:audioConverter];
}
//在写一次cookie,有时编解码器会在转换结束时更新 cookie
[self writeMagicCookie:audioConverter];
}
writePacketTableInfo_inTrailer
函数如下
/**
kAudioConverterPrimeInfo:AudioConverter的启动信息。一些音频数据格式转换,特别是那些涉及采样率转换的音频数据格式转换,
当有leadingFrames或trailingFrames可用时,会产生更高质量的输出。 这些启动信息的适当数量取决于输入的音频数据格式。
*/
- (void)writePacketTableInfo_inTrailer:(AudioConverterRef)converter {
/**
mNumberPackets是包含在文件中的音频数据的总的分组数;
mPrimingFrames是经过分组(“packetized”)的流用作准备和/或处理等待时间的帧数;
mRemainderFrames是最后分组遗留下的帧数。例如,AAC位流可能仅有在其最后分组中有效的313帧。每分组的帧为1024,所以,在此情形,mRemainderFrames是(1024-313),其表示了当进行解码时应该从最后分组的输出中裁剪下来的样本数。
如果经过编码的位流正被编辑,那么就推荐在将会占据至少mPrimingFrames的编辑点之前的分组应该被所述编辑所采用,以从编辑点中确保音频的完美再现。当然,在随机访问文件中的不同分组以便播放时,mPrimingFrames就应该被用来在所想要的点上重新构造音频。
*/
UInt32 size = 0;
UInt32 isWritable;
OSStatus status = AudioFileGetPropertyInfo(_outFile, kAudioFilePropertyPacketTableInfo, &size, &isWritable);
printErr(@"AudioFileGetPropertyInfo kAudioFilePropertyPacketTableInfo", status);
if (noErr != status || isWritable == 0) {
NSLog(@"AudioFileGetPropertyInfo kAudioFilePropertyPacketTableInfo failed return");
return;
}
/**
UInt32 leadingFrames; -> 0
UInt32 trailingFrames; -> 1368
*/
AudioConverterPrimeInfo primeInfo;
size = sizeof(primeInfo);
status = AudioConverterGetProperty(converter, kAudioConverterPrimeInfo, &size, &primeInfo);
printErr(@"AudioConverterGetProperty kAudioConverterPrimeInfo", status);
if (status !=noErr) return;
/**
SInt64 mNumberValidFrames; -> 442368
SInt32 mPrimingFrames; -> 0
SInt32 mRemainderFrames; -> 0
*/
AudioFilePacketTableInfo pTableInfo;
size = sizeof(pTableInfo);
status = AudioFileGetProperty(_outFile, kAudioFilePropertyPacketTableInfo, &size, &pTableInfo);
printErr(@"AudioFileGetProperty kAudioFilePropertyPacketTableInfo", status);
if (status !=noErr) return;
//获取总数量的帧数
UInt64 totalFrames = pTableInfo.mNumberValidFrames + pTableInfo.mPrimingFrames + pTableInfo.mRemainderFrames;
pTableInfo.mPrimingFrames = primeInfo.leadingFrames;
pTableInfo.mRemainderFrames = primeInfo.trailingFrames;
pTableInfo.mNumberValidFrames = totalFrames - pTableInfo.mPrimingFrames - pTableInfo.mRemainderFrames;
NSLog(@"table info 里面包含的总数量的帧数: %llu,\t mNumberValidFrames:%lld,\t mPrimingFrames:%d,\t mRemainderFrames:%d", totalFrames, pTableInfo.mNumberValidFrames, pTableInfo.mPrimingFrames, pTableInfo.mRemainderFrames);
/**
SInt64 mNumberValidFrames; -> 441100
SInt32 mPrimingFrames; -> 0
SInt32 mRemainderFrames; -> 1368 (1368 不够组成一个 flac packet,所以被遗留下来?)
*/
status = AudioFileSetProperty(_outFile, kAudioFilePropertyPacketTableInfo, sizeof(pTableInfo), &pTableInfo);
printErr(@"AudioFileSetProperty kAudioFilePropertyPacketTableInfo", status);
}
15. 释放相关内存
NSLog(@"convert 结束");
if (outBuffer) {
free(outBuffer);
outBuffer = NULL;
}
if(_inBuffer) {
free(_inBuffer);
_inBuffer = NULL;
}
if (_outFile_2) {
fclose(_outFile_2);
_outFile_2 = NULL;
}
//关闭和释放AudioConverter的资源
JBAssertNoError( AudioConverterDispose(audioConverter),@"AudioConverterFillComplexBuffer");
if (_inFile) {
JBAssertNoError(AudioFileClose(_inFile), @"AudioFileClose in");
_inFile = NULL;
}
if (_outFile) {
JBAssertNoError(AudioFileClose(_outFile),@"AudioFileClose out");
_outFile = NULL;
}
NSLog(@"所有结束\n");
self.isRunning = NO;
16. 打印输出文件
调用辅助函数,打印两个输出文件,并可以使用 打印中的log,直接在 命令行 工具中进行播放,验证效果
[JBHelper prisnFFmpegLogWithASBD:self.outASBD path:[self.outFileURL1.absoluteString stringByReplacingOccurrencesOfString:@"file://" withString:@""] preLog:@"apple caf file:\t"];
[JBHelper prisnFFmpegLogWithASBD:self.outASBD path:[self.outFileURL2.absoluteString stringByReplacingOccurrencesOfString:@"file://" withString:@""] preLog:@"c write pcm file:\t"];
打印如下
apple caf file: ffplay /Users/jimbo/Library/Caches/com.jimbo.mac.coreaudio.CoreAudioDemo/output.caf
c write pcm file: ffplay -ar 44100 -ac 2 -f s16be /Users/jimbo/Library/Caches/com.jimbo.mac.coreaudio.CoreAudioDemo/pcm_output.pcm
直接使用
ffplay
命令播放
pcm
文件.
ffplay
需要使用
brew install ffmpeg
安装
最终,我们的音频编解码完成了。
原版地址:
http://t.csdn.cn/RA9dv