Apple AudioToolbox 之 音频编解码(AudioConverterRef)

今天记录是的是 使用 AudioToolbox 框架 使用 AudioConverterRef 工具进行本地音频文件的编码和解码。
本文打仓库代码为: JBLocalAudioFileConvecter

分别实现了:

  • flac,mp3等其他音频编码文件 转换成 pcm文件。 (解码)
  • pcm文件 转换成 flac,mp3等其他音频编码文件。 (编码)
    两者的代码基本一样,只是在输入和输出的时候对应的 静态码率(CBR)和动态码率(VBR),在处理和取值的时候有所区别。

运行本demo对应的ui 界面的按钮入口为:
在这里插入图片描述
在这里插入图片描述

1. 全局变量

列出来头部 所申明的所有全局属性和成员变量。都有说明。

@interface JBLocalAudioFileConvecter()
{
@publicAudioFileID _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. 创建和打开输入文件

分别创建了两个输出文件,进行测试对比

    //打开 输出 音频 文件1JBAssertNoError(AudioFileCreateWithURL((__bridge CFURLRef)self.outFileURL1, kAudioFileCAFType, &_outASBD, kAudioFileFlags_EraseFile, &_outFile),@"AudioFileCreateWithURL");//打开 输出 音频 文件2if (_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 和部分编解码所需要的数据。
我们可以看下下图, flacpcm 解码出来的文件里面的 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));//这里是AudioFileGetPropertyJBAssertNoError(AudioFileGetProperty(_inFile, kAudioFilePropertyMagicCookieData, &cookieDataSize, cookieData),@"AudioFileGetProperty kAudioFilePropertyMagicCookieData");//将获取的MagicCookie 设置到 converter 中JBAssertNoError(AudioConverterSetProperty(converter,kAudioConverterDecompressionMagicCookie,cookieDataSize,cookieData),@"AudioConverterSetProperty kAudioConverterDecompressionMagicCookie");// malloc 后必须 freefree(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 aacsize = 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中,就是我们编解码好的数据,我们将 调用AudioFileWritePacketsfwrite 分别写入 输出文件1 和 2 中。两者写入时候传的参数有所区别。
  • 最后不要完了,outFilePacketOffset += ioOutDataPacketsPerOut;, 不然下次写入的位置不对
    UInt64 totalOutputFrames_debug = 0;UInt32 outFilePacketOffset = 0; //输出文件的写入偏移量OSStatus status = noErr;//阻塞式while (true) {if (!_isRunning) {break;}//创建输出的AudioBufferAudioBufferList 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, //输出的bufferself.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);//写入文件 到 AudioFileJBAssertNoError(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;//aspdif (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;  ->  0UInt32      trailingFrames; -> 1368*/AudioConverterPrimeInfo primeInfo;size = sizeof(primeInfo);status = AudioConverterGetProperty(converter, kAudioConverterPrimeInfo, &size, &primeInfo);printErr(@"AudioConverterGetProperty kAudioConverterPrimeInfo", status);if (status !=noErr) return;/**SInt64  mNumberValidFrames;  ->  442368SInt32  mPrimingFrames;      ->  0SInt32  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;  ->  441100SInt32  mPrimingFrames;      ->  0SInt32  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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/31484.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

macos搭建appium-iOS自动化测试环境

目录 准备工作 安装必需的软件 安装appium 安装XCode 下载WDA工程 配置WDA工程 搭建appiumwda自动化环境 第一步&#xff1a;启动通过xcodebuild命令启动wda服务 分享一下如何在mac电脑上搭建一个完整的appium自动化测试环境 准备工作 前期需要准备的设备和账号&…

【深度学习笔记】TensorFlow 常用函数

TensorFlow 提供了一些机器学习中常用的数学函数&#xff0c;并封装在 Module 中&#xff0c;例如 tf.nn Module 提供了神经网络常用的基本运算&#xff0c;tf.math Module 则提供了机器学习中常用的数学函数。本文主要介绍 TensorFlow 深度学习中几个常用函数的定义与用法&…

机器学习---监督学习和非监督学习

根据训练期间接受的监督数量和监督类型&#xff0c;可以将机器学习分为以下四种类型&#xff1a;监督学习、非监督学习、半监督学习和强化学习。 监督学习 在监督学习中&#xff0c;提供给算法的包含所需解决方案的训练数据&#xff0c;成为标签或标记。 简单地说&#xff0c;…

IoTDB 小白“踩坑”心得:入门安装部署篇

小伙伴介绍&#xff01; 大家好&#xff0c;我是 zai&#xff0c;一个基本功不那么扎实、没有太多经验的大学生。我刚刚加入社区&#xff0c;接触 IoTDB&#xff0c;目前仍处于学习阶段&#xff0c;所以我会跟大家分享我学习过程中踩过的一些雷&#xff0c;以及对应的解决办法&…

超低功耗在智能门锁行业的应用

1. 名词解释 在本体上以电子方式识别、处理人体生物特征信息、电子信息、网络通讯信息等并控制机械执行机构实施启闭的门锁”叫电子智能门锁。通俗地理解&#xff0c;智能门锁是电子信息技术与机械技术相结合的全新的锁具品类&#xff0c;是在传统机械锁基础上升级改进的&…

SpringBoot运行流程源码分析------阶段二(run方法核心流程)

run方法核心流程 在分析和学习整个run方法之前&#xff0c;我们可以通过以下流程图来看下SpringApplication调用的run方法处理的核心操作包含哪些。 从上面的流程图中可以看出&#xff0c;SpringApplication在run方法中重点做了以下几步操作 获取监听器和参数配置打印banner…

.NET6使用SqlSugar操作数据库

1.//首先引入SqlSugarCore包 2.//新建SqlsugarSetup类 public static class SqlsugarSetup{public static void AddSqlsugarSetup(this IServiceCollection services, IConfiguration configuration,string dbName "ConnectString"){SqlSugarScope sqlSugar new Sq…

函数的递归

1、什么是递归&#xff1f; 程序调用自身的编程技巧称为递归。 递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法&#xff0c;它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解&#x…

CM11 链表分割 题解

题目描述&#xff1a; 链表分割_牛客题霸_牛客网 (nowcoder.com) 现有一链表的头指针 ListNode* pHead&#xff0c;给一定值x&#xff0c;编写一段代码将所有小于x的结点排在其余结点之前&#xff0c;且不能改变原来的数据顺序&#xff0c;返回重新排列后的链表的头指针。 题解…

工业4.0:欢迎来到智能制造

制造业正在经历一场被称为“工业4.0”的全新技术革命&#xff0c;这场革命将数字化、网络化、智能化和自动化技术融合在一起&#xff0c;旨在打造高质、高效、高产且可持续的智能工厂。工业4.0将彻底改变产品制造的方式&#xff0c;颠覆我们对制造业的传统认知。 什么是工业4.…

MRO工业品采购过程中,采购人员要注意哪些事项

MRO工业品指工厂或企业对其生产和工作设施、设备进行保养、维修&#xff0c;保证其运行所需要的非生产性物料&#xff0c;这些物料可能是用于设备保养、维修的备品备件&#xff0c;也可能是保证企业正常运行的相关设备&#xff0c;耗材等物资&#xff0c;如安全防护、传媒广电、…

(一)创建型设计模式:3、建造者模式(Builder Pattern)

目录 1、建造者模式含义 2、建造者模式的讲解 3、使用C实现建造者模式的实例 4、建造者模式的优缺点 5、建造者模式VS工厂模式 1、建造者模式含义 The intent of the Builder design pattern is to separate the construction of a complex object from its representatio…

检测ChatGPT生成内容的工具经常误报,怪不得OpenAI停用它

​近日&#xff0c;加利福尼亚大学戴维斯分校的学生被指控使用ChaGPT作弊。他的老师指控他在历史考试中使用了ChatGPT&#xff0c;这一指控得到了某生成式内容检测工具的支持。然而&#xff0c;该名学生坚决否认这一指控&#xff0c;他通过提供在线文档的编辑日志为自己洗清了嫌…

Git仓关联多个远程仓路径

前言 Git仓如果需要将代码push到多个仓&#xff0c;常用的做法是添加多个远程仓路径&#xff0c;然后分别push。这样虽然可以实现目的&#xff0c;但是需要多次执行push指令&#xff0c;很麻烦。 本文介绍关联多个远程仓路径且执行一次push指令的方法&#xff1a;git remote …

【Docker】Docker私有仓库的使用

目录 一、搭建私有仓库 二、上传镜像到私有仓库 三、从私有仓库拉取镜像 一、搭建私有仓库 首先我们需要拉取仓库的镜像 docker pull registry 然后创建私有仓库容器 docker run -it --namereg -p 5000:5000 registry 这个时候我们可以打开浏览器访问5000端口看是否成功&…

微服务与Nacos概述-3

流量治理 在微服务架构中将业务拆分成一个个的服务&#xff0c;服务与服务之间可以相互调用&#xff0c;但是由于网络原因或者自身的原因&#xff0c;服务并不能保证服务的100%可用&#xff0c;如果单个服务出现问题&#xff0c;调用这个服务就会出现网络延迟&#xff0c;此时…

Redis_主从复制

8. 主从复制 8.1 简介 主从库采用读写分离的方式 读操作&#xff1a;主库、从库都可以处理写操作&#xff1a;首先写到主库执行&#xff0c;然后再将主库同步给从库。 实现读写分离&#xff0c;性能扩展 容灾快速恢复 8.2 主从复制步骤 创建一个目录 ,在root下创建一个m…

hive on tez资源控制

sql insert overwrite table dwintdata.dw_f_da_enterprise2 select * from dwintdata.dw_f_da_enterprise; hdfs文件大小数量展示 注意这里文件数有17个 共计321M 最后是划分为了21个task 为什么会有21个task&#xff1f;不是128M 64M 或者说我这里小于128 每个文件一个map…

(C++)继承

目录 1.继承的概念及定义 1.1继承的概念 1.2继承定义 1.2.1定义格式 1.2.2继承方式和访问限定符 1.2.3继承基类成员访问方式的变化 2.基类和派生类对象赋值转换 3.继承中的作用域 4.派生类的默认成员函数 5.继承与友元 6.继承与静态成员 7.复杂的菱形继承及菱形虚拟…

【脚踢数据结构】链表(1)

(꒪ꇴ꒪ )&#xff0c;Hello我是祐言QAQ我的博客主页&#xff1a;C/C语言,Linux基础,ARM开发板&#xff0c;软件配置等领域博主&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的攻城狮&#xff01;送给自己和读者的一句鸡汤&#x1f914;&…