今天记录是的是 使用 AudioToolbox
框架 使用 AudioConverterRef
工具进行本地音频文件的编码和解码。
本文打仓库代码为: JBLocalAudioFileConvecter
分别实现了:
flac
,mp3
等其他音频编码文件 转换成 pcm
文件。 (解码)pcm
文件 转换成 flac
,mp3
等其他音频编码文件。 (编码)运行本demo对应的ui 界面的按钮入口为:
在这里插入图片描述
列出来头部 所申明的所有全局属性和成员变量。都有说明。
@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
这里输入一个文件,然后输出了两种文件,两种输出文件的写入方式稍微有点区别。
并且 工作的显示是在 workQueue
的子线程中,不阻塞主线程。
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];
});
}
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;
}
工作函数(子线程) 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);\
}\
}
设置到全局变量 _inASBD
里
UInt32 size = sizeof(AudioStreamBasicDescription);
JBAssertNoError(AudioFileGetProperty(_inFile, kAudioFilePropertyDataFormat, &size, &_inASBD), @"AudioFileGetProperty kAudioFilePropertyDataFormat");
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;
}
分别创建了两个输出文件,进行测试对比
//打开 输出 音频 文件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++");
}
根据输入和输出的 ASBD
进行创建绑定
//创建转换对象
AudioConverterRef audioConverter;
JBAssertNoError(AudioConverterNew(&_inASBD, &_outASBD, &audioConverter),@"AudioConverterNew");
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);
}
ASBD 设置到 AudioConverter 后,被在里面被自动修正某些有问题的值, 所以我们重新从里面获取修正后的值
//重新从 Audio Convert 获取被校正过的 ASBD数据
JBAssertNoError(AudioConverterGetProperty(audioConverter, kAudioConverterCurrentInputStreamDescription, &size, &_inASBD), @"kAudioConverterCurrentInputStreamDescription get infile");
JBAssertNoError(AudioConverterGetProperty(audioConverter, kAudioConverterCurrentOutputStreamDescription, &size, &_outASBD), @"kAudioConverterCurrentInputStreamDescription get outfile");
大体流程如下
_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;
}
这里输出缓冲区的大小和输入一样,当然也可以设置成不一样的值。
如果 输出是 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));
}
写入 第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);
}
这里就是 最重要的 读取文件,编解码,写入文件的环节了。
一个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
在上一步 AudioConverterFillComplexBuffer
中,我们配置了这函数,系统会在特定时候调用这个函数,我们只需要读取特定大小的数据,写入ioData
中,也就是上一步的 outBufferList
中。
ioNumberDataPackets
判断,读取的数量是否 会超出我们开辟的空间大小AudioFileReadPacketData
然后调用函数进行读取文件,并输出到_inBuffer
这个全局变量中,jbClass.inPacketReadIndex
输入文件的偏移量往后移ioData
根据自己实际获取的数据,填充这个对象。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;
}
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);
}
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;
调用辅助函数,打印两个输出文件,并可以使用 打印中的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
安装
最终,我们的音频编解码完成了。
原版地址:https://blog.csdn.net/goldWave01/article/details/132215342