原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
续文见上篇:IOS企业:司机端APP行驶全程录音功能(上)
目录
- 六、录音文件加密
- 1、进行加密
- 2、进行解密
- 七、上传录音文件
- 1、获取待上传文件的路径
- 2、通过委托方法进行音频文件的实时上传
- 3、每5分钟自动检测并上传遗留文件
- 八、处理录制中断事件
- 1、启动时删除录音中断文件的recording标志
- 2、将中断文件转化为UCAR文件
- 3、判断中断类型
- 九、内存溢出覆盖最早的录音
- 1、计算录音文件总的大小
- 2、内存溢出时自动覆盖最早的录音文件
- 十、音频知识
- 1、音频压缩编码格式
- 2、lame静态库
- Demo
- 参考文献
六、录音文件加密
需求:生成的音频文件需要加密保存,司机端本地不可查看/检索/播放
1、进行加密
为方便更快的测试,这里的录音时长没有配置为3分钟,而是5秒。
2020-11-20 14:22:12.530726+0800 Demo[43609:1409708] 成功在原文件夹生成加密后的文件
2020-11-20 14:22:12.531061+0800 Demo[43609:1409708] 成功删除未加密的mp3原始录音文件
a、使用RNEncryptor进行文件加密
加密方式采用的是默认的AES256
。
- (NSString *)encryptedRecorderDataWithFilePath:(NSString *)recorderFilePath encryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix
{
// 需要加密的音频文件数据
NSData *recorderFileData = [NSData dataWithContentsOfFile:recorderFilePath];
// 错误
NSError *error = nil;
// RNCryptor加密
NSData *encryptedRecorderFileData;
if (encryptKey.length > 0)
{
encryptedRecorderFileData = [RNEncryptor encryptData:recorderFileData withSettings:kRNCryptorAES256Settings password: encryptKey error:&error];
}
.......
}
b、更改录音文件格式
音频文件转化后为mp3
格式。如果只是加密而不转化音频文件的格式则文件仍然为mp3
文件,点击后仍然能够打开播放。所以需要将mp3
文件后缀名修改为其他格式,默认为UCAR
格式。
- (NSString *)encryptedRecorderDataWithFilePath:(NSString *)recorderFilePath encryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix
{
.......
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = recorderFilePath;
NSString *modifySuffixRecorderFilePath;
if (modifySuffix.length > 0)
{
modifySuffixRecorderFilePath = [path stringByReplacingOccurrencesOfString:@"mp3" withString:modifySuffix];
}
.......
}
c、在原文件夹生成加密后的文件
不能用更换文件名的方式生成新文件,因为文件内容已经变成了encryptedRecorderFileData
即加密后的数据,所以需要通过该数据内容生成新的加密文件,再删除掉原文件。
[fileManager moveItemAtPath:recorderFilePath toPath:deleteEndTimeFilePath error:nil];
因为传过来的mp3
文件是正在录制的,路径中包含recording
标识,在我们加密完成后需要将该标识删除,变成录制完成的文件。
- (NSString *)encryptedRecorderDataWithFilePath:(NSString *)recorderFilePath encryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix
{
.......
// 在原文件夹生成加密后的文件
if (![fileManager createFileAtPath:modifySuffixRecorderFilePath contents:encryptedRecorderFileData attributes:nil])
{
.......
}
else
{
NSLog(@"成功在原文件夹生成加密后的文件");
// 删除未加密的mp3原始录音文件
[fileManager removeItemAtPath:recorderFilePath error:&error];
if (error == nil)
{
NSLog(@"成功删除未加密的mp3原始录音文件");
}
else
{
NSLog(@"删除源文件失败的错误信息为:%@",error);
NSString *failDeleteMP3FilePath = recorderFilePath;
if ([recorderFilePath containsString:@"recording"])
{
failDeleteMP3FilePath = [self deleteRecordingTagWithFilePath:recorderFilePath];
}
NSLog(@"抱歉,删除未加密的mp3原始录音文件失败,该文件转为完成状态,路径为:%@",failDeleteMP3FilePath);
}
// 删除给录制中的文件添加的.recording后缀变成录制完成的文件
NSString *UCARFilePath = modifySuffixRecorderFilePath;
if ([modifySuffixRecorderFilePath containsString:@"recording"])
{
UCARFilePath = [self deleteRecordingTagWithFilePath:modifySuffixRecorderFilePath];
}
// 返回生成的加密文件的路径
return UCARFilePath;
}
}
d、加密失败的文件处理
加密失败通常是由于传入的recorderFilePath
、encryptKey
、modifySuffix
为空。
- (NSString *)encryptedRecorderDataWithFilePath:(NSString *)recorderFilePath encryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix
{
.......
if (![fileManager createFileAtPath:modifySuffixRecorderFilePath contents:encryptedRecorderFileData attributes:nil])
{
// 这样写是因为createFileAtPath这个方法只返回了一个布尔值,并没有具体的错误信息,使用errno可以解决这个问题
NSLog(@"加密错误码: %d - 加密错误信息: %s", errno, strerror(errno));
// 删除给录制中的文件添加的.recording后缀变成录制完成的文件
NSString *failMP3Path = recorderFilePath;
if ([recorderFilePath containsString:@"recording"])
{
failMP3Path = [self deleteRecordingTagWithFilePath:recorderFilePath];
}
NSLog(@"加密失败的MP3的路径 = %@",failMP3Path);
return nil;
}
}
2、进行解密
功能:对目录下的所有UCAR音频文件进行解密,作测试用。
a、调用时机
这个方法在AppDelegate
中就可以调用了。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[UCARRecordSoundTool shareUCARRecordSoundTool] decryptAllUCARRecorderFilesWithEncryptKey:@"" modifySuffix:@""];
}
b、防止encryptKey和modifySuffix为空,导致加密和解密失败
因为是在AppDelegate
中调用的,而此时录音器的参数都还没有配置,所以encryptKey
和modifySuffix
为空,就会导致崩溃。
2020-11-20 16:05:39.871513+0800 Demo[46679:1521721] *** Assertion failure in -[RNDecryptor initWithPassword:handler:], RNDecryptor.m:147
2020-11-20 16:05:51.092180+0800 Demo[46679:1521721] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: aPassword != nil'
所以在第一次开始录音的时候就可以将这两个参数保存到本地的Recorder.plist
文件中。
-(void)startRecordWithOrderNumber:(NSString *)orderNumber driverID:(NSString *)driverID
{
.......
NSDictionary *dict = @{@"encryptKey": self.encryptKey, @"modifySuffix": self.modifySuffix, @"sampleRate": @(self.sampleRate)};
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *recorderPlistPath = [documentPath stringByAppendingPathComponent:@"Recorder.plist"];
NSURL *recorderPlistPathUrl = [NSURL fileURLWithPath:recorderPlistPath];
if ( [dict writeToURL:recorderPlistPathUrl atomically:YES] )
{
NSLog(@"加密密钥成功写入Plist文件,路径为:%@",recorderPlistPath);
}
.......
}
如果这两个参数值为空的话则直接从Recorder.plist
文件中取值。
2020-11-20 15:54:52.526856+0800 Demo[46541:1512103] 从录音Plist文件中读取到的字典为:{
encryptKey = "U2FsdGVkX1+21W0Epk68cW2rlAt/TuHcDO4A+UYtbjI=";
modifySuffix = UCAR;
sampleRate = 11025;
}
- (void)decryptAllUCARRecorderFilesWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix
{
// 防止encryptKey和modifySuffix为空,导致加密和解密失败
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *recorderPlistPath = [documentPath stringByAppendingPathComponent:@"Recorder.plist"];
NSDictionary *dictionaryFromRecorderPlist = [NSDictionary dictionaryWithContentsOfFile:recorderPlistPath];
NSLog(@"从录音Plist文件中读取到的字典为:%@",dictionaryFromRecorderPlist);
if (encryptKey == nil || [encryptKey isEqualToString:@""])
{
encryptKey = dictionaryFromRecorderPlist[@"encryptKey"];
}
if (modifySuffix == nil || [modifySuffix isEqualToString:@""])
{
modifySuffix = dictionaryFromRecorderPlist[@"modifySuffix"];
}
......
}
c、获取到所有加密的UCAR文件
先获取目录下所有UCAR
文件的名称。
2020-11-20 16:05:39.871087+0800 Demo[46679:1521721] 所有UCAR文件:(
"35200505324217_2890893_1605858208000_MP3.UCAR",
"35200505324217_2890893_1605853332000_MP3.UCAR",
"35200505324217_2890893_1605858214000_MP3.UCAR",
"35200505324217_2890893_1605858208000_1605858213000_MP3.UCAR"
)
- (NSArray *)getAllUCARRecorderFiles
{
NSFileManager *manager = [NSFileManager defaultManager];
// 获得当前文件的所有子文件:subpathsAtPath:
NSArray *pathList = [manager subpathsAtPath:recordFilePath];
NSMutableArray *UCARAudioPathList = [NSMutableArray array];
// 遍历这个文件夹下面的子文件,获得所有UCAR文件
for (NSString *audioPath in pathList)
{
// UCAR文件
if ([audioPath.pathExtension isEqualToString:self.modifySuffix])
{
[UCARAudioPathList addObject:audioPath];
}
}
NSLog(@"所有UCAR文件:%@",UCARAudioPathList);
.......
}
再获取目录下所有UCAR
文件的路径。
- (NSArray *)getAllUCARRecorderFiles
{
.......
// 存储UCAR文件的路径列表
NSMutableArray *UCARAudioFilePathList = [NSMutableArray array];
if (UCARAudioPathList.count > 0)
{
for (NSString *audioPath in UCARAudioPathList)
{
// 每个UCAR录音文件的路径
NSString *UCARRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
[UCARAudioFilePathList addObject:UCARRecordFilePath];
}
}
// 返回UCAR文件的路径列表
return [UCARAudioFilePathList copy];
}
d、对获取到的所有UCAR文件进行解密
2020-11-20 16:24:54.856469+0800 Demo[46852:1534949] 成功在原文件夹生成解密后的文件
2020-11-20 16:24:54.856877+0800 Demo[46852:1534949] 成功删除加密的录音文件
- (void)decryptAllUCARRecorderFilesWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix
{
.......
NSArray *UCARAudioList = [[UCARAudioTool shareUCARAudioTool] getAllUCARRecorderFiles];
if (UCARAudioList.count > 0)
{
for (NSString *UCARRecorderFilePath in UCARAudioList)
{
NSData *UCARRecorderFileData = [NSData dataWithContentsOfFile:UCARRecorderFilePath];
// RNCryptor解密
NSError *error = nil;
NSData *decryptRecorderFileData = [RNDecryptor decryptData:UCARRecorderFileData withPassword:encryptKey error:&error];
// 更改录音文件格式
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = UCARRecorderFilePath;
NSString *modifySuffixRecorderFilePath = [path stringByReplacingOccurrencesOfString:modifySuffix withString:@"mp3"];
// 在原文件夹生成加密后的文件
if (![fileManager createFileAtPath:modifySuffixRecorderFilePath contents:decryptRecorderFileData attributes:nil])
{
// 这样写是因为createFileAtPath这个方法只返回了一个布尔值,并没有具体的错误信息,使用errno可以解决这个问题
NSLog(@"解密错误码: %d - 解密错误信息: %s", errno, strerror(errno));
}
else
{
NSLog(@"成功在原文件夹生成解密后的文件");
[fileManager removeItemAtPath:UCARRecorderFilePath error:&error];
if (error == nil)
{
NSLog(@"成功删除加密的录音文件");
}
}
}
}
}
七、上传录音文件
1、获取待上传文件的路径
a、caf转化为mp3之后调用的方法
- (void)beginRecordWithRecordName:(NSString *)recordName withRecordType:(NSString *)type withIsConventToMp3:(BOOL)isConventToMp3
{
......
[[UCARLameTool shareUCARLameTool] audioRecodingToMP3:weakSelf.recordPath isDeleteSourchFile:YES withSuccessBack:^(NSString * _Nonnull resultPath) {
NSLog(@"转 MP3 成功");
NSLog(@"转为MP3后的路径 = %@",resultPath);
[self successConvertToMP3WithFilePath:resultPath];
} withFailBack:^(NSString * _Nonnull error) {
NSLog(@"转 MP3 失败");
// 删除给录制中的文件添加的.recording后缀变成录制完成的文件
NSString *failCafFilePath = weakSelf.recordPath;
if ([weakSelf.recordPath containsString:@"recording"])
{
failCafFilePath = [self deleteRecordingTagWithFilePath:weakSelf.recordPath];
}
[self failConvertToMP3WithFilePath:failCafFilePath];
}];
}
......
}
b、转化mp3成功后再将其转化为UCAR文件
如果生成加密文件失败却还未结束行程则直接返回并录制下一段音频。
- (void)successConvertToMP3WithFilePath:(NSString *)resultPath
{
// 生成的音频文件需要加密保存,司机端本地不可查看/检索/播放
NSString *modifySuffixRecorderFilePath = [self encryptedRecorderDataWithFilePath:resultPath encryptKey:self.encryptKey modifySuffix:self.modifySuffix];
// 生成加密文件失败则直接返回
if (modifySuffixRecorderFilePath == nil || [modifySuffixRecorderFilePath isEqualToString:@""])
{
if (!self.isEndTrip)
{
[self restartRecord];
}
return;
}
.......
}
c、获取上传文件的路径
- 尚未结束行程时,保存成功则录制下一段,上传路径为原始加密文件的路径
- 结束行程时未满3分钟需要给录音文件重新命名,上传路径为重命名后加密文件的路径
- (void)successConvertToMP3WithFilePath:(NSString *)resultPath
{
.......
// 上传文件的路径
NSString *uploadFilePath;
// 尚未结束行程时,保存成功则录制下一段
if (!self.isEndTrip)
{
uploadFilePath = modifySuffixRecorderFilePath;
[self restartRecord];
}
// 结束行程时未满3分钟需要给录音文件重新命名
if (self.isEndTrip)
{
NSString *renameEndTripRecordingFilePath = [self renameEndTripRecordingFileWithFilePath:modifySuffixRecorderFilePath];
NSLog(@"结束行程时未满3分钟需要给录音文件重新命名,修改后地址为:%@",renameEndTripRecordingFilePath);
uploadFilePath = renameEndTripRecordingFilePath;
}
.......
}
d、caf转化为mp3失败之后的操作
- 尚未结束行程时,则录制下一段
- 结束行程时未满3分钟需要给录音文件重新命名,录音文件路径为重命名后的
caf
文件路径
- (void)failConvertToMP3WithFilePath:(NSString *)cafFilePath
{
// 录音文件每3分钟保存一个文件,保存成功则录制下一段,时长可配置
if (!self.isEndTrip)
{
[self restartRecord];
}
// 结束行程时未满3分钟需要给录音文件重新命名
if (self.isEndTrip)
{
NSString *renameEndTripRecordingFilePath = [self renameEndTripRecordingFileWithFilePath:cafFilePath];
NSLog(@"结束行程时未满3分钟需要给录音文件重新命名,修改后地址为:%@",renameEndTripRecordingFilePath);
}
}
2、通过委托方法进行音频文件的实时上传
- 音频文件每录制完成一个则自动进行上传
- 上传成功后删除司机端本地文件
录音文件的委托
@protocol AudioToolDelegate <NSObject>
/// 上传录音文件的委托方法
- (void)uploadRecordingFileWithEncryptedRecorderFilePath:(NSString *)uploadFilePath;
@end
/** 委托 */
@property (nonatomic, weak) id<AudioToolDelegate> delegate;
调用上传录音文件的方法
- (void)successConvertToMP3WithFilePath:(NSString *)resultPath
{
if (self.delegate && [self.delegate respondsToSelector:@selector(uploadRecordingFileWithEncryptedRecorderFilePath:)])
{
[self.delegate uploadRecordingFileWithEncryptedRecorderFilePath:uploadFilePath];
}
}
在委托类中实现该委托方法
用于录制完时间间隔为3分钟的音频文件后自动上传的方法。
@interface UCARDirverUploadFileTool ()<UCARRecordSoundToolDelegate>
@end
- (void)uploadRecordingFileWithEncryptedRecorderFilePath:(NSString *)uploadFilePath
{
// 没有在上传则进行上传录音文件流程
if (![UCARDirverUploadFileTool shareUCARDirverUploadFileTool].isUploading)
{
// 可以使用uploadFilePath参数上传当前录制完成的加密文件,也可以使用startUploadTask上传所有的加密文件
[[UCARDirverUploadFileTool shareUCARDirverUploadFileTool] startUploadTask];
}
}
3、每5分钟自动检测并上传遗留文件
a、启动定时上传录音
司机端定时检测间隔(s),默认5分钟检测一次。
- (void)uploadTaskWithFireTime
{
NSTimeInterval time = 300;
if (self.config.scanInterval)
{
time = self.config.scanInterval;
}
// 启动定时上传
[self startUploadTask];
if (!self.timer)
{
self.timer = [NSTimer scheduledTimerWithTimeInterval:time repeats:YES block:^(NSTimer * _Nonnull timer) {
[self startUploadTask];
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
}
b、开启上传录音任务
- 转换所有文件为待上传状态
- 获取待上传的文件
- 批量上传
- (void)startUploadTask
{
// 正在上传则直接返回
if (self.isUploading)
{
return;
}
NSLog(@"检测录音文件");
// 转换所有文件为待上传状态
[[UCARRecordSoundTool shareUCARRecordSoundTool] convertAudioToUCARWithEncryptKey:@"U2FsdGVkX1+21W0Epk68cW2rlAt/TuHcDO4A+UYtbjI=" modifySuffix:@"UCAR" sampleRate:11025];
// 获取待上传的文件
[self.pathArr removeAllObjects];
NSMutableArray *pathArr = [[UCARRecordSoundTool shareUCARRecordSoundTool] getAllUCARRecorderFiles].mutableCopy;
if (pathArr.count == 0)
{
NSLog(@"没有找到待上传的录音文件");
return;
}
[self.pathArr addObjectsFromArray:pathArr];
NSLog(@"找到待上传的录音文件, 准备上传");
WeakSelf(weakSelf);
[self startUploadItemsCompletion:^(NSMutableArray *successResultPath) {
NSLog(@"本次批量上传成功%lu个录音", (unsigned long)successResultPath.count);
StrongSelf(strongSelf);
strongSelf.isUploading = NO;
}];
}
c、批量上传录音文件
待上传的文件数为0则直接返回。
- (void)startUploadItemsCompletion:(void(^)(NSMutableArray *successResultPath))completion
{
// 待上传的文件数为0则直接返回
if (self.pathArr.count < 1)
{
return;
}
......
}
准备保存上传成功的录音文件名称的数组,用于知道哪些文件上传成功了。
- (void)startUploadItemsCompletion:(void(^)(NSMutableArray *successResultPath))completion
{
// 元素个数与上传的图片个数相同,先用 NSNull 占位
NSMutableArray* result = [NSMutableArray array];
for (NSInteger i = 0; i<self.pathArr.count; i++)
{
[result addObject:[NSNull null]];
}
for (int i = 0; i<self.pathArr.count; i++)
{
NSString *name = [self.pathArr[i] lastPathComponent];
[self startUploadItem:UCARRecorderFileData name:name completion:^(BOOL success) {
// 上传成功
if (success)
{
// 加入上传成功的文件名称
@synchronized (result) {
// NSMutableArray 不是线程安全的,所以加个同步锁
result[i] = name;
}
}
}];
}
}
通过dispatch_group
批量逐个上传录音文件,当全部上传完成后再返回上传的结果。
- (void)startUploadItemsCompletion:(void(^)(NSMutableArray *successResultPath))completion
{
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i<self.pathArr.count; i++)
{
dispatch_group_enter(group);
// 待上传的录音文件的数据和名称
NSData *UCARRecorderFileData = [NSData dataWithContentsOfFile:self.pathArr[i]];
WeakSelf(weakSelf);
self.isUploading = YES;// 设置上传状态为正在上传
[self startUploadItem:UCARRecorderFileData name:name completion:^(BOOL success) {
StrongSelf(strongSelf);
// 上传成功
if (success)
{
// 删除已上传文件
if (strongSelf.pathArr.count > 0)
{
NSString *filePath = strongSelf.pathArr[i];
[[UCARRecordSoundTool shareUCARRecordSoundTool] deleteRecordFileWithFilePath:filePath];
dispatch_group_leave(group);
}
}
else
{
dispatch_group_leave(group);
}
}];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (completion)
{
completion(result);
}
});
}
d、上传单个录音文件
上传录音文件的具体实现方式这里只是做个参考,不同项目封装的实现方式各不相同。
- (void)startUploadItem:(id)item name:(NSString *)name completion:(void(^)(BOOL success))completion
{
NSLog(@"录音文件开始上传");
UCARHttpRequestConfig *config = [UCARHttpRequestConfig defaultConfig];
config.subURL = UCAR_HTTP_RECORD_UPLOADFILE;
config.postDataFormatBlock = ^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:item name:@"record" fileName:name mimeType:@"UCAR"];
};
[[UCARHttpManager sharedManager] asyncPostWithConfig:config success:^(id _Nonnull response, NSDictionary * _Nullable request) {
NSLog(@"[recordUpload]上传成功的文件名:%@", name);
if (completion) {
completion(YES);
}
} failure:^(id _Nullable response, NSDictionary * _Nullable request, NSError * _Nonnull error) {
NSLog(@"[recordUpload]上传失败的文件名:%@", name);
if (completion) {
completion(NO);
}
}];
}
八、处理录制中断事件
1、启动时删除录音中断文件的recording标志
a、删除recording标志和结束时间
- 中断录音需要删除不确定的结束时间
- 删除录制中的文件的
.recording
表示该文件已经录制完成
- (NSString *)deleteRecordingTagAndEndTimeWithFilePath:(NSString *)recorderFilePath
{
// 删除recording Tag
......
// 删除结束录音时间
endRecordTime = [NSString stringWithFormat:@"_%@",endRecordTime];
NSString *deleteEndTimeFilePath = [deleteRecordingTagFilePath stringByReplacingOccurrencesOfString:endRecordTime withString:@""];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager moveItemAtPath:recorderFilePath toPath:deleteEndTimeFilePath error:nil];
return deleteEndTimeFilePath;
}
b、使所有中断的文件变成录音完成状态的文件
- (void)convertRecordingFileToFinishedFile
{
NSLog(@"因为录音中断,将recording标志删除掉,变成录音完成的文件");
NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) firstObject];
NSString *recordFilePath = [libraryPath stringByAppendingString:@"/Caches/Recorder/"];
NSLog(@"录音文件目录路径为:%@",recordFilePath);
NSFileManager *manager = [NSFileManager defaultManager];
// 获得当前文件的所有子文件:subpathsAtPath:
NSArray *pathList = [manager subpathsAtPath:recordFilePath];
// 遍历这个文件夹下面的子文件,获得因中断等原因未自动转换成功的caf文件和mp3文件
for (NSString *audioPath in pathList)
{
if ([audioPath containsString:@"recording"])
{
// 每个正在录音的文件的路径
NSString *recordingFilePath = [recordFilePath stringByAppendingString:audioPath];
// 因为录音中断,将recording标志删除掉,变成录音完成的文件
// 同时删除结束录音时间
[self deleteRecordingTagAndEndTimeWithFilePath:recordingFilePath];
}
}
}
c、调用时机
该方法在APP启动的时候调用,因为中断都会退出APP,留下这些还在录制中的音频文件。
所以需要在启动APP的时候就将这些正处于录制状态中的遗留文件变为录制完成状态的文件,之后在每次5分钟的扫描时进行转化和上传。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[UCARRecordSoundTool shareUCARRecordSoundTool] convertRecordingFileToFinishedFile];
}
2、将中断文件转化为UCAR文件
a、获得因中断等原因未自动转换成功的caf文件和mp3文件
2020-11-20 18:26:22.496560+0800 Demo[53521:1642212] 未成功生成mp3的剩余caf文件:(
"35200505324217_2890893_1605865955000_MP3.caf",
"35200505324217_2890893_1605865955000_MP3.caf",
)
2020-11-20 18:26:22.496642+0800 Demo[53521:1642212] 未成功生成加密文件的剩余mp3文件:(
"35200505324217_2890893_1605865955000_MP3.mp3",
"35200505324217_2890893_1605865944000_1605865949000_MP3.mp3"
)
未成功生成加密文件的剩余mp3
文件。
- (NSArray *)convertAudioToUCARWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix sampleRate:(int)sampleRate
{
// 防空处理
.......
// 遍历这个文件夹下面的子文件,获得因中断等原因未自动转换成功的caf文件和mp3文件
for (NSString *audioPath in pathList)
{
if (![audioPath containsString:@"recording"])
{
// 未成功生成加密文件的剩余mp3文件
if ([audioPath.pathExtension isEqualToString:@"mp3"])
{
[mp3AudioPathList addObject:audioPath];
}
}
}
NSLog(@"未成功生成加密文件的剩余mp3文件:%@",mp3AudioPathList);
}
未成功生成mp3
的剩余caf
文件。需要注意的是,caf
文件转化为mp3
文件后可能和之前边录制边转化的mp3
文件重了,导致在对两个相同的mp3
文件进行加密时,后加密的那个mp3
文件判断为文件已经存在,则报错。所以需要判断caf
文件名称和mp3
文件名称是否相等,相等说明是同一个录音文件则直接删除即可。
- (NSArray *)convertAudioToUCARWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix sampleRate:(int)sampleRate
{
for (NSString *audioPath in pathList)
{
if (![audioPath containsString:@"recording"])
{
// 未成功生成mp3的剩余caf文件
if ([audioPath.pathExtension isEqualToString:@"caf"])
{
NSString *cafAudioName = [audioPath stringByDeletingPathExtension];
for (NSString *mp3AudioPath in mp3AudioPathList)
{
NSString *mp3AudioName = [mp3AudioPath stringByDeletingPathExtension];
if ([cafAudioName isEqualToString:mp3AudioName])// 相等说明是同一个录音文件则直接删除即可
{
// 每个caf录音文件的路径
NSString *cafRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
[self deleteRecordFileWithFilePath:cafRecordFilePath];
}
else// 否则加入caf待转录列表
{
[cafAudioPathList addObject:audioPath];
}
}
}
}
}
NSLog(@"未成功生成mp3的剩余caf文件:%@",cafAudioPathList);
}
b、将获得的caf文件和mp3文件进行加密转化为UCAR文件
将剩余caf
文件转化为UCAR
。
- (NSArray *)convertAudioToUCARWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix sampleRate:(int)sampleRate
{
.......
// 存储转化而成的UCAR文件的数据用于上传
NSMutableArray *UCARAudioPathList = [NSMutableArray array];
// 将剩余caf文件转化为UCAR
if (cafAudioPathList.count > 0)
{
for (NSString *audioPath in cafAudioPathList)
{
// 每个caf录音文件的路径
NSString *cafRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
// 采样率
[UCARLameTool shareUCARLameTool].sampleRate = sampleRate;
// 转为MP3
[[UCARLameTool shareUCARLameTool] audioToMP3:cafRecordFilePath isDeleteSourchFile:YES withSuccessBack:^(NSString * _Nonnull resultPath) {
NSLog(@"转为MP3后的路径 = %@",resultPath);
// 将mp3文件进行加密
NSString *encryptedRecorderDataWithFilePath = [self encryptedRecorderDataWithFilePath:resultPath encryptKey:encryptKey modifySuffix:modifySuffix];
if (encryptedRecorderDataWithFilePath && ![encryptedRecorderDataWithFilePath isEqualToString:@""])
{
[UCARAudioPathList addObject:encryptedRecorderDataWithFilePath];
}
} withFailBack:^(NSString * _Nonnull error) {
NSLog(@"将caf文件转换为mp3文件失败:%@",error);
}];
}
}
.......
}
将剩余mp3
文件转化为UCAR
。
- (NSArray *)convertAudioToUCARWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix sampleRate:(int)sampleRate
{
.......
if (mp3AudioPathList.count > 0)
{
for (NSString *audioPath in mp3AudioPathList)
{
// 每个mp3录音文件的路径
NSString *mp3RecordFilePath = [recordFilePath stringByAppendingString:audioPath];
// 将mp3文件进行加密
NSString *encryptedRecorderDataWithFilePath = [self encryptedRecorderDataWithFilePath:mp3RecordFilePath encryptKey:encryptKey modifySuffix:modifySuffix];
if (encryptedRecorderDataWithFilePath && ![encryptedRecorderDataWithFilePath isEqualToString:@""])
{
[UCARAudioPathList addObject:encryptedRecorderDataWithFilePath];
}
}
}
.......
}
返回转化而成的UCAR
文件的路径列表用于上传。
return [UCARAudioPathList copy];
c、调用时机
该方法在APP启动的时候调用,因为中断都会退出APP,留下这些转化失败的音频文件,所以启动的时候可以将其全部转化为UCAR
文件。需要注意的是,一定要在调用了convertRecordingFileToFinishedFile
之后进行调用。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[UCARRecordSoundTool shareUCARRecordSoundTool] convertRecordingFileToFinishedFile];
[[UCARRecordSoundTool shareUCARRecordSoundTool] convertAudioToUCARWithEncryptKey:@"" modifySuffix:@"" sampleRate:0];
return YES;
}
这些因为中断而导致的录音文件会没有准确的结束时间,可以和正常完成录音的音频文件相区分。
3、判断中断类型
a、注册音频录制中断通知
- (AVAudioRecorder *)audioRecorder
{
__weak typeof(self) weakSelf = self;
if (!_audioRecorder)
{
// 注册音频录制中断通知
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(handleNotification:) name:AVAudioSessionInterruptionNotification object:nil];
......
}
return _audioRecorder;
}
b、接收录制中断事件通知,并处理相关事件
监听诸如系统来电,闹钟响铃,Facetime……导致的音频中断终端事件。
- (void)handleNotification:(NSNotification *)notification
{
NSArray *allKeys = notification.userInfo.allKeys;
// 判断事件类型
if([allKeys containsObject:AVAudioSessionInterruptionTypeKey])
{
AVAudioSessionInterruptionType audioInterruptionType = [[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] integerValue];
switch (audioInterruptionType)
{
case AVAudioSessionInterruptionTypeBegan:
NSLog(@"录音被打断……开始");
break;
case AVAudioSessionInterruptionTypeEnded:
NSLog(@"录音被打断……结束");
break;
}
}
// 判断中断的音频录制是否可恢复录制
if([allKeys containsObject:AVAudioSessionInterruptionOptionKey])
{
AVAudioSessionInterruptionOptions shouldResume = [[notification.userInfo valueForKey:AVAudioSessionInterruptionOptionKey] integerValue];
if(shouldResume)
{
NSLog(@"录音被打断……结束。可以恢复录音了");
}
}
}
c、移除通知
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
九、内存溢出覆盖最早的录音
1、计算录音文件总的大小
a、获取所有的音频文件
计算的内存是包括所有类型的音频文件占用的空间大小的总和,而不只是加密后的UCAR
文件,所以需要将录音完成状态的caf
、mp3
、UCAR
所有文件全部获得。
2020-11-23 10:21:45.841072+0800 Demo[83676:3085221] 所有的音频文件:(
"35200505324217_2890893_1606098081000_1606098086000_MP3.UCAR",
"35200505324217_2890893_1606098092000_1606098097000_MP3.UCAR",
"35200505324217_2890893_1606098097000_1606098102000_MP3.UCAR",
"35200505324217_2890893_1606098087000_1606098092000_MP3.UCAR"
)
不能获取还处于录音状态中的文件,因为录音状态中的文件的大小处于不停变化的状态。
2020-11-23 10:21:45.840982+0800 Demo[83676:3085221] 获得当前文件的所有子文件:(
"35200505324217_2890893_1606098081000_1606098086000_MP3.UCAR",
"recording_35200505324217_2890893_1606098087000_1606098092000_MP3.mp3",
".DS_Store",
"recording_35200505324217_2890893_1606098103000_1606098108000_MP3.mp3",
"35200505324217_2890893_1606098092000_1606098097000_MP3.UCAR",
"35200505324217_2890893_1606098097000_1606098102000_MP3.UCAR",
"35200505324217_2890893_1606098087000_1606098092000_MP3.UCAR",
"recording_35200505324217_2890893_1606098103000_1606098108000_MP3.caf",
"recording_35200505324217_2890893_1606098092000_1606098097000_MP3.mp3",
"recording_35200505324217_2890893_1606098097000_1606098102000_MP3.mp3",
"recording_35200505324217_2890893_1606098081000_1606098086000_MP3.mp3"
)
实现的代码如下:
- (double)calculationRecordFileSizeSum
{
// 遍历这个文件夹下面的子文件,只获得录音文件
for (NSString *audioPath in pathList)
{
if (![audioPath containsString:@"recording"])
{
// 成功生成的加密文件
BOOL isUCAR = [audioPath.pathExtension isEqualToString:self.modifySuffix];
// 未成功生成加密文件的剩余mp3文件
BOOL isFailMp3 = [audioPath.pathExtension isEqualToString:@"mp3"];
// 未成功生成mp3的剩余caf文件
BOOL isFailCaf = [audioPath.pathExtension isEqualToString:@"caf"];
// 通过对比文件的延展名(扩展名、尾缀)来区分是不是录音文件
if (isUCAR || isFailMp3 || isFailCaf)
{
// 把筛选出来的文件放到数组中 -> 得到所有的音频文件
[audioPathList addObject:audioPath];
}
}
}
NSLog(@"获得当前文件的所有子文件:%@",pathList);
NSLog(@"所有的音频文件:%@",audioPathList);
......
}
b、计算所有的音频文件大小
2020-11-23 10:21:45.841421+0800 Demo[83676:3085221] 所有的音频文件大小为:0.068947MB
通过获取文件的大小属性来进行累加,再将其转化为MB为单位。
- (double)calculationRecordFileSizeSum
{
.......
double allRecordFileSize = 0;
for (NSString *audioPath in audioPathList)
{
// 每个录音文件的路径
NSString *everyRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
// 每个录音文件的大小
NSNumber *everyRecordFileSize = [manager attributesOfItemAtPath:everyRecordFilePath error:nil][NSFileSize];
// 所有录音文件的大小
allRecordFileSize += [everyRecordFileSize doubleValue];
}
allRecordFileSize = allRecordFileSize / 1024.0 / 1024.0;
NSLog(@"所有的音频文件大小为:%fMB",allRecordFileSize);
return allRecordFileSize;
}
2、内存溢出时自动覆盖最早的录音文件
a、用于判断的两个条件
条件一:所有的音频文件限制大小。录音文件最大占用内存大小,以MB为单位,可配置,默认1024MB,即1个G。
NSLog(@"所有的音频文件限制大小为:%fMB",maximumMemory);
2020-11-23 10:21:27.009733+0800 Demo[83676:3085221] 所有的音频文件限制大小为:1024.000000MB
条件二:未使用的磁盘空间大小。
2020-11-23 10:21:27.010731+0800 Demo[83676:3085221] 磁盘空闲空间为: 157657.08 MB == 153.96 GB
- (double)getFreeDiskSpace
{
NSError *error = nil;
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:&error];
if (error) return -1;
int64_t space = [[attrs objectForKey:NSFileSystemFreeSize] longLongValue];
if (space < 0) space = -1;
NSString *freeDiskInfo = [NSString stringWithFormat:@" %.2f MB == %.2f GB", space/1024/1024.0, space/1024/1024/1024.0];
NSLog(@"磁盘空闲空间为:%@",freeDiskInfo);
double freeDisk = space/1024/1024.0;
return freeDisk;
}
b、未溢出则直接返回
- 已经录制的所有的音频文件大小如果小于音频文件限制大小则表示还需要继续录制
- 如果大于系统可用存储空间的最小值(暂定为100MB)则表示系统还允许继续录制
- (void)coverEarliestRecordFileWithMemoryLimit:(double)maximumMemory
{
// 所有的音频文件大小
double allRecordFileSize = [self calculationRecordFileSizeSum];
if (allRecordFileSize < maximumMemory && [self getFreeDiskSpace] > 100)
{
return;
}
......
}
c、按照录音文件的录制开始时间进行升序排序
当配置所有的音频文件限制大小为0.1MB时候的输出结果如下。
[UCARRecordSoundTool shareUCARRecordSoundTool].maximumMemory = 0.1;
2020-11-23 10:42:30.400225+0800 Demo[84637:3133118] 所有的音频文件大小为:0.313307MB
2020-11-23 10:42:30.400764+0800 Demo[84637:3133118] 排序后的所有的音频文件:(
"35200505324217_2890893_1606098081000_MP3.mp3",
"35200505324217_2890893_1606098087000_MP3.UCAR",
"35200505324217_2890893_1606098092000_1606098097000_MP3.UCAR",
"35200505324217_2890893_1606098097000_1606098102000_MP3.UCAR",
......
这样排序后的列表中的第一个文件就是最早录制的音频文件。
- (void)coverEarliestRecordFileWithMemoryLimit:(double)maximumMemory
{
......
NSArray *orderedAudioPathList = [self.audioPathList sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
// 获取文件名 20201010-055153-20201012035704.UCAR
NSString *fileName1 = obj1;
NSString *fileName2 = obj2;
// 获取开始时间 20201012035704
NSArray *file1Component = [fileName1 componentsSeparatedByString:@"_"];
NSString *number1 = file1Component[2];
NSArray *file2Component = [fileName2 componentsSeparatedByString:@"_"];
NSString *number2 = file2Component[2];
// 比较integerValue
if ([number1 integerValue] > [number2 integerValue])
{
return NSOrderedDescending;
}
else if ([number1 integerValue] < [number2 integerValue])
{
return NSOrderedAscending;
}
else
{
return NSOrderedSame;
}
}];
NSLog(@"排序后的所有的音频文件:%@",orderedAudioPathList);
......
}
d、覆盖最早的录音,其实就是删除最早的录音
2020-11-23 10:42:30.402078+0800 Demo[84637:3133118] 成功删除最早的录音文件
- (void)coverEarliestRecordFileWithMemoryLimit:(double)maximumMemory
{
......
NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) firstObject];
NSString *recordFilePath = [libraryPath stringByAppendingString:@"/Caches/Recorder/"];
NSString *earliestRecordFilePath = [recordFilePath stringByAppendingString:orderedAudioPathList[0]];
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtPath:earliestRecordFilePath error:&error];
if (error == nil)
{
NSLog(@"成功删除最早的录音文件");
}
}
e、调用时机
音频文件在司机端本地最多占用1024M存储空间,空间满时自动覆盖生成时间最早的文件,空间上限可配置。
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
{
.......
[self coverEarliestRecordFileWithMemoryLimit:self.maximumMemory];
NSLog(@"录音结束");
}
十、音频知识
1、音频压缩编码格式
a、压缩需求
我们通常从音乐App(如:网易云音乐)听歌时,会看到一首歌需要的存储空间大概是10M左右,对于手机磁盘来说这是可以接受的。但在网络中实时在线传播的话,这个数据量可能就太大了,所以必须对其进行压缩编码。压缩编码的基本指标之一就是压缩比,压缩比通常小于1(否则就没有必要去做压缩)。
b、压缩算法
包含无损压缩和有损压缩,常用压缩格式中,用的较多的是有损压缩。
无损压缩:解压后的数据可以完全复原。
有损压缩:解压后的数据不能完全复原,会丢失一部分信息,压缩比越小,丢失的信息就越多,信号还原后失真就会越大。
c、压缩编码原理
压缩编码原理实际上是压缩掉冗余信号,冗余信号是指不能被人耳感知到的信号,包含人耳听觉范围之外的音频信号以及被掩蔽掉的音频信号。
d、常用压缩编码格式
PCM编码
PCM
编码是没有压缩的音频数据,也可以叫音频裸数据。音频的裸数据格式就是脉冲编码调制(Pulse Code Modulation
,PCM
)数据,是按照一定的格式记录采样和量化后的数字数据,描述一段PCM
数据需要这几个概念——量化格式(sampleFormat
)、采样率(sampleRate
)、声道数(channel
)。
采样率是指自然界的音频即声波转换为数字数据保存时单位时间采样个数。采样率越高,精确度越大。人对频率的识别范围是 20HZ - 20000HZ
。所以22050
的采样频率是常用的音频采样率,而44100
采样率即是CD级别。16bit pcm
意味着使用两个字节去保存采样值。采样数据记录的是振幅,采样精度取决于储存空间的大小。2 字节(也就是16bit
) 65536个等级 , CD级别,16bit pcm
就是最常见的。
WAV编码
WAV
编码有多种实现方式,但是都不会进行压缩操作。在PCM
数据格式的前面加上44字节,分别用来描述PCM
的采样率、声道数、数据格式等信息。
MP3编码
MP3
具有不错的压缩比,使用LAME
编码(MP3
编码格式的一种实现)的中高码率的MP3
文件,听感上非常接近源WAV
文件。音质在128Kbit/s
以上表现还不错,压缩比较高,兼容性好,用于音乐欣赏。
AAC编码
新一代的音频有损压缩技术,在小于128Kbit/s
的码率下表现优异,并且多用于视频中音频轨的编码。
Ogg编码
一种非常有潜力的编码。Ogg
有着非常出色的算法,可以用更小的码率达到更好的音质,128Kbit/s
的Ogg
比192Kbit/s
甚至更高码率的MP3
还要出色。Ogg目前受支持的情况还不够好,适用语音聊天。
2、lame静态库
a、通讯格式
IM项目中涉及语音通讯,需要选择一款优良的通讯格式。由于iOS原生不支持录制AMR
格式和MP3
格式,但是这两个格式是目前移动端比较喜爱的选择。最开始倾向于AMR
语音通讯,因为AMR
体积很小,很省流量,但是PC端播放AMR
同样需要转码,耗时且体验不好,最后选择中庸但流行的MP3
作为语音通讯的桥梁。
b、编码器
因为iOS没有原生录制编码为MP3
和转码MP3
的功能,需要三方库支持,目前最成熟且最广泛的转码库为lame
库。LAME
是一个开源的MP3
音频压缩软件,可以将音频裸PCM
数据编码成mp3
,目前是公认有损品质MP3
中压缩效果最好的编码器。
AMR
和MP3
录制与转换都需要用到三方库,但MP3
还需要自己编译并构建Lame
静态库,于是在项目中引入了lame.h
文件和libmp3lame.a
静态库框架。
- 需要将
lame
打包转化为可用于App
的静态库引入项目。 - 音频处理:录制时已经采用最低端质量录制
AVAudioQualityMin
,采样率为8000HZ,声道数为单声道。 - 利用
lame
库将wav
或者caf
转换mp3
后,体积有明显减小,音质也能保证清晰流畅。
c、lame生成静态库
- 下载 lame 的最新版本并解压到桌面的一个文件夹里例如
lame
。 - 为了把下载的
lame
生成静态库,需要下载 build 的脚本,下载之后将得到lame-build.sh
拷贝到上一步解压好的文件夹里。
- 使用文本编辑打开
built-lame.sh
,修改脚本为可执行脚本并在终端执行脚本。
SOURCE=""
FAT="fat-lame"
SCRATCH="/Users/xiejiapei/Desktop/lame"
xiejiapei@xiejiapeis-iMac lame % chmod 777 build-lame.sh
xiejiapei@xiejiapeis-iMac lame % ./build-lame.sh
building arm64...
- 会生成支持多种架构的
fat-lame
文件,简称胖文件,把fat-lame
里面的lame.h
与libmp3lame.a
导入工程即可。
- 导入编译完成后的静态库到工程,使用代码都写在
LameTool
类里面。
Demo
Demo在我的Github上,欢迎下载。
EnterpriseDemo