原創(chuàng):知識(shí)探索型文章
創(chuàng)作不易灰追,請(qǐng)珍惜递览,之后會(huì)持續(xù)更新,不斷完善
個(gè)人比較喜歡做筆記和寫總結(jié)疯溺,畢竟好記性不如爛筆頭哈哈论颅,這些文章記錄了我的IOS成長(zhǎng)歷程,希望能與大家一起進(jìn)步
溫馨提示:由于簡(jiǎn)書不支持目錄跳轉(zhuǎn)囱嫩,大家可通過(guò)command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容
續(xù)文見(jiàn)上篇:IOS企業(yè):司機(jī)端APP行駛?cè)啼浺艄δ埽ㄉ希?/a>
目錄
- 六恃疯、錄音文件加密
- 1、進(jìn)行加密
- 2墨闲、進(jìn)行解密
- 七今妄、上傳錄音文件
- 1、獲取待上傳文件的路徑
- 2鸳碧、通過(guò)委托方法進(jìn)行音頻文件的實(shí)時(shí)上傳
- 3盾鳞、每5分鐘自動(dòng)檢測(cè)并上傳遺留文件
- 八、處理錄制中斷事件
- 1杆兵、啟動(dòng)時(shí)刪除錄音中斷文件的recording標(biāo)志
- 2雁仲、將中斷文件轉(zhuǎn)化為UCAR文件
- 3仔夺、判斷中斷類型
- 九绒尊、內(nèi)存溢出覆蓋最早的錄音
- 1榨咐、計(jì)算錄音文件總的大小
- 2、內(nèi)存溢出時(shí)自動(dòng)覆蓋最早的錄音文件
- 十、音頻知識(shí)
- 1威酒、音頻壓縮編碼格式
- 2、lame靜態(tài)庫(kù)
- Demo
- 參考文獻(xiàn)
六蝴蜓、錄音文件加密
需求:生成的音頻文件需要加密保存鞭缭,司機(jī)端本地不可查看/檢索/播放
1、進(jìn)行加密
為方便更快的測(cè)試抛猖,這里的錄音時(shí)長(zhǎng)沒(méi)有配置為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進(jìn)行文件加密
加密方式采用的是默認(rèn)的AES256
联四。
- (NSString *)encryptedRecorderDataWithFilePath:(NSString *)recorderFilePath encryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix
{
// 需要加密的音頻文件數(shù)據(jù)
NSData *recorderFileData = [NSData dataWithContentsOfFile:recorderFilePath];
// 錯(cuò)誤
NSError *error = nil;
// RNCryptor加密
NSData *encryptedRecorderFileData;
if (encryptKey.length > 0)
{
encryptedRecorderFileData = [RNEncryptor encryptData:recorderFileData withSettings:kRNCryptorAES256Settings password: encryptKey error:&error];
}
.......
}
b撑碴、更改錄音文件格式
音頻文件轉(zhuǎn)化后為mp3
格式。如果只是加密而不轉(zhuǎn)化音頻文件的格式則文件仍然為mp3
文件朝墩,點(diǎn)擊后仍然能夠打開(kāi)播放醉拓。所以需要將mp3
文件后綴名修改為其他格式,默認(rèn)為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亿卤、在原文件夾生成加密后的文件
不能用更換文件名的方式生成新文件,因?yàn)槲募?nèi)容已經(jīng)變成了encryptedRecorderFileData
即加密后的數(shù)據(jù)鹿霸,所以需要通過(guò)該數(shù)據(jù)內(nèi)容生成新的加密文件排吴,再刪除掉原文件。
[fileManager moveItemAtPath:recorderFilePath toPath:deleteEndTimeFilePath error:nil];
因?yàn)閭鬟^(guò)來(lái)的mp3
文件是正在錄制的懦鼠,路徑中包含recording
標(biāo)識(shí)傍念,在我們加密完成后需要將該標(biāo)識(shí)刪除,變成錄制完成的文件葛闷。
- (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(@"刪除源文件失敗的錯(cuò)誤信息為:%@",error);
NSString *failDeleteMP3FilePath = recorderFilePath;
if ([recorderFilePath containsString:@"recording"])
{
failDeleteMP3FilePath = [self deleteRecordingTagWithFilePath:recorderFilePath];
}
NSLog(@"抱歉憋槐,刪除未加密的mp3原始錄音文件失敗,該文件轉(zhuǎn)為完成狀態(tài)淑趾,路徑為:%@",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])
{
// 這樣寫是因?yàn)閏reateFileAtPath這個(gè)方法只返回了一個(gè)布爾值,并沒(méi)有具體的錯(cuò)誤信息延蟹,使用errno可以解決這個(gè)問(wèn)題
NSLog(@"加密錯(cuò)誤碼: %d - 加密錯(cuò)誤信息: %s", errno, strerror(errno));
// 刪除給錄制中的文件添加的.recording后綴變成錄制完成的文件
NSString *failMP3Path = recorderFilePath;
if ([recorderFilePath containsString:@"recording"])
{
failMP3Path = [self deleteRecordingTagWithFilePath:recorderFilePath];
}
NSLog(@"加密失敗的MP3的路徑 = %@",failMP3Path);
return nil;
}
}
2评矩、進(jìn)行解密
功能:對(duì)目錄下的所有UCAR音頻文件進(jìn)行解密,作測(cè)試用阱飘。
a斥杜、調(diào)用時(shí)機(jī)
這個(gè)方法在AppDelegate
中就可以調(diào)用了。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[UCARRecordSoundTool shareUCARRecordSoundTool] decryptAllUCARRecorderFilesWithEncryptKey:@"" modifySuffix:@""];
}
b沥匈、防止encryptKey和modifySuffix為空蔗喂,導(dǎo)致加密和解密失敗
因?yàn)槭窃?code>AppDelegate中調(diào)用的,而此時(shí)錄音器的參數(shù)都還沒(méi)有配置高帖,所以encryptKey
和modifySuffix
為空缰儿,就會(huì)導(dǎo)致崩潰。
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'
所以在第一次開(kāi)始錄音的時(shí)候就可以將這兩個(gè)參數(shù)保存到本地的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);
}
.......
}
如果這兩個(gè)參數(shù)值為空的話則直接從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為空,導(dǎo)致加密和解密失敗
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];
// 獲得當(dāng)前文件的所有子文件:subpathsAtPath:
NSArray *pathList = [manager subpathsAtPath:recordFilePath];
NSMutableArray *UCARAudioPathList = [NSMutableArray array];
// 遍歷這個(gè)文件夾下面的子文件,獲得所有UCAR文件
for (NSString *audioPath in pathList)
{
// UCAR文件
if ([audioPath.pathExtension isEqualToString:self.modifySuffix])
{
[UCARAudioPathList addObject:audioPath];
}
}
NSLog(@"所有UCAR文件:%@",UCARAudioPathList);
.......
}
再獲取目錄下所有UCAR
文件的路徑默终。
- (NSArray *)getAllUCARRecorderFiles
{
.......
// 存儲(chǔ)UCAR文件的路徑列表
NSMutableArray *UCARAudioFilePathList = [NSMutableArray array];
if (UCARAudioPathList.count > 0)
{
for (NSString *audioPath in UCARAudioPathList)
{
// 每個(gè)UCAR錄音文件的路徑
NSString *UCARRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
[UCARAudioFilePathList addObject:UCARRecordFilePath];
}
}
// 返回UCAR文件的路徑列表
return [UCARAudioFilePathList copy];
}
d椅棺、對(duì)獲取到的所有UCAR文件進(jìn)行解密
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])
{
// 這樣寫是因?yàn)閏reateFileAtPath這個(gè)方法只返回了一個(gè)布爾值,并沒(méi)有具體的錯(cuò)誤信息齐蔽,使用errno可以解決這個(gè)問(wèn)題
NSLog(@"解密錯(cuò)誤碼: %d - 解密錯(cuò)誤信息: %s", errno, strerror(errno));
}
else
{
NSLog(@"成功在原文件夾生成解密后的文件");
[fileManager removeItemAtPath:UCARRecorderFilePath error:&error];
if (error == nil)
{
NSLog(@"成功刪除加密的錄音文件");
}
}
}
}
}
七两疚、上傳錄音文件
1、獲取待上傳文件的路徑
a含滴、caf轉(zhuǎn)化為mp3之后調(diào)用的方法
- (void)beginRecordWithRecordName:(NSString *)recordName withRecordType:(NSString *)type withIsConventToMp3:(BOOL)isConventToMp3
{
......
[[UCARLameTool shareUCARLameTool] audioRecodingToMP3:weakSelf.recordPath isDeleteSourchFile:YES withSuccessBack:^(NSString * _Nonnull resultPath) {
NSLog(@"轉(zhuǎn) MP3 成功");
NSLog(@"轉(zhuǎn)為MP3后的路徑 = %@",resultPath);
[self successConvertToMP3WithFilePath:resultPath];
} withFailBack:^(NSString * _Nonnull error) {
NSLog(@"轉(zhuǎn) MP3 失敗");
// 刪除給錄制中的文件添加的.recording后綴變成錄制完成的文件
NSString *failCafFilePath = weakSelf.recordPath;
if ([weakSelf.recordPath containsString:@"recording"])
{
failCafFilePath = [self deleteRecordingTagWithFilePath:weakSelf.recordPath];
}
[self failConvertToMP3WithFilePath:failCafFilePath];
}];
}
......
}
b诱渤、轉(zhuǎn)化mp3成功后再將其轉(zhuǎn)化為UCAR文件
如果生成加密文件失敗卻還未結(jié)束行程則直接返回并錄制下一段音頻。
- (void)successConvertToMP3WithFilePath:(NSString *)resultPath
{
// 生成的音頻文件需要加密保存谈况,司機(jī)端本地不可查看/檢索/播放
NSString *modifySuffixRecorderFilePath = [self encryptedRecorderDataWithFilePath:resultPath encryptKey:self.encryptKey modifySuffix:self.modifySuffix];
// 生成加密文件失敗則直接返回
if (modifySuffixRecorderFilePath == nil || [modifySuffixRecorderFilePath isEqualToString:@""])
{
if (!self.isEndTrip)
{
[self restartRecord];
}
return;
}
.......
}
c勺美、獲取上傳文件的路徑
- 尚未結(jié)束行程時(shí),保存成功則錄制下一段碑韵,上傳路徑為原始加密文件的路徑
- 結(jié)束行程時(shí)未滿3分鐘需要給錄音文件重新命名赡茸,上傳路徑為重命名后加密文件的路徑
- (void)successConvertToMP3WithFilePath:(NSString *)resultPath
{
.......
// 上傳文件的路徑
NSString *uploadFilePath;
// 尚未結(jié)束行程時(shí),保存成功則錄制下一段
if (!self.isEndTrip)
{
uploadFilePath = modifySuffixRecorderFilePath;
[self restartRecord];
}
// 結(jié)束行程時(shí)未滿3分鐘需要給錄音文件重新命名
if (self.isEndTrip)
{
NSString *renameEndTripRecordingFilePath = [self renameEndTripRecordingFileWithFilePath:modifySuffixRecorderFilePath];
NSLog(@"結(jié)束行程時(shí)未滿3分鐘需要給錄音文件重新命名祝闻,修改后地址為:%@",renameEndTripRecordingFilePath);
uploadFilePath = renameEndTripRecordingFilePath;
}
.......
}
d占卧、caf轉(zhuǎn)化為mp3失敗之后的操作
- 尚未結(jié)束行程時(shí),則錄制下一段
- 結(jié)束行程時(shí)未滿3分鐘需要給錄音文件重新命名联喘,錄音文件路徑為重命名后的
caf
文件路徑
- (void)failConvertToMP3WithFilePath:(NSString *)cafFilePath
{
// 錄音文件每3分鐘保存一個(gè)文件华蜒,保存成功則錄制下一段,時(shí)長(zhǎng)可配置
if (!self.isEndTrip)
{
[self restartRecord];
}
// 結(jié)束行程時(shí)未滿3分鐘需要給錄音文件重新命名
if (self.isEndTrip)
{
NSString *renameEndTripRecordingFilePath = [self renameEndTripRecordingFileWithFilePath:cafFilePath];
NSLog(@"結(jié)束行程時(shí)未滿3分鐘需要給錄音文件重新命名豁遭,修改后地址為:%@",renameEndTripRecordingFilePath);
}
}
2叭喜、通過(guò)委托方法進(jìn)行音頻文件的實(shí)時(shí)上傳
- 音頻文件每錄制完成一個(gè)則自動(dòng)進(jìn)行上傳
- 上傳成功后刪除司機(jī)端本地文件
錄音文件的委托
@protocol AudioToolDelegate <NSObject>
/// 上傳錄音文件的委托方法
- (void)uploadRecordingFileWithEncryptedRecorderFilePath:(NSString *)uploadFilePath;
@end
/** 委托 */
@property (nonatomic, weak) id<AudioToolDelegate> delegate;
調(diào)用上傳錄音文件的方法
- (void)successConvertToMP3WithFilePath:(NSString *)resultPath
{
if (self.delegate && [self.delegate respondsToSelector:@selector(uploadRecordingFileWithEncryptedRecorderFilePath:)])
{
[self.delegate uploadRecordingFileWithEncryptedRecorderFilePath:uploadFilePath];
}
}
在委托類中實(shí)現(xiàn)該委托方法
用于錄制完時(shí)間間隔為3分鐘的音頻文件后自動(dòng)上傳的方法。
@interface UCARDirverUploadFileTool ()<UCARRecordSoundToolDelegate>
@end
- (void)uploadRecordingFileWithEncryptedRecorderFilePath:(NSString *)uploadFilePath
{
// 沒(méi)有在上傳則進(jìn)行上傳錄音文件流程
if (![UCARDirverUploadFileTool shareUCARDirverUploadFileTool].isUploading)
{
// 可以使用uploadFilePath參數(shù)上傳當(dāng)前錄制完成的加密文件蓖谢,也可以使用startUploadTask上傳所有的加密文件
[[UCARDirverUploadFileTool shareUCARDirverUploadFileTool] startUploadTask];
}
}
3捂蕴、每5分鐘自動(dòng)檢測(cè)并上傳遺留文件
a、啟動(dòng)定時(shí)上傳錄音
司機(jī)端定時(shí)檢測(cè)間隔(s)蜈抓,默認(rèn)5分鐘檢測(cè)一次启绰。
- (void)uploadTaskWithFireTime
{
NSTimeInterval time = 300;
if (self.config.scanInterval)
{
time = self.config.scanInterval;
}
// 啟動(dòng)定時(shí)上傳
[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昂儒、開(kāi)啟上傳錄音任務(wù)
- 轉(zhuǎn)換所有文件為待上傳狀態(tài)
- 獲取待上傳的文件
- 批量上傳
- (void)startUploadTask
{
// 正在上傳則直接返回
if (self.isUploading)
{
return;
}
NSLog(@"檢測(cè)錄音文件");
// 轉(zhuǎn)換所有文件為待上傳狀態(tài)
[[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(@"沒(méi)有找到待上傳的錄音文件");
return;
}
[self.pathArr addObjectsFromArray:pathArr];
NSLog(@"找到待上傳的錄音文件, 準(zhǔn)備上傳");
WeakSelf(weakSelf);
[self startUploadItemsCompletion:^(NSMutableArray *successResultPath) {
NSLog(@"本次批量上傳成功%lu個(gè)錄音", (unsigned long)successResultPath.count);
StrongSelf(strongSelf);
strongSelf.isUploading = NO;
}];
}
c沟使、批量上傳錄音文件
待上傳的文件數(shù)為0則直接返回。
- (void)startUploadItemsCompletion:(void(^)(NSMutableArray *successResultPath))completion
{
// 待上傳的文件數(shù)為0則直接返回
if (self.pathArr.count < 1)
{
return;
}
......
}
準(zhǔn)備保存上傳成功的錄音文件名稱的數(shù)組渊跋,用于知道哪些文件上傳成功了腊嗡。
- (void)startUploadItemsCompletion:(void(^)(NSMutableArray *successResultPath))completion
{
// 元素個(gè)數(shù)與上傳的圖片個(gè)數(shù)相同着倾,先用 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 不是線程安全的,所以加個(gè)同步鎖
result[i] = name;
}
}
}];
}
}
通過(guò)dispatch_group
批量逐個(gè)上傳錄音文件燕少,當(dāng)全部上傳完成后再返回上傳的結(jié)果卡者。
- (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);
// 待上傳的錄音文件的數(shù)據(jù)和名稱
NSData *UCARRecorderFileData = [NSData dataWithContentsOfFile:self.pathArr[i]];
WeakSelf(weakSelf);
self.isUploading = YES;// 設(shè)置上傳狀態(tài)為正在上傳
[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、上傳單個(gè)錄音文件
上傳錄音文件的具體實(shí)現(xiàn)方式這里只是做個(gè)參考客们,不同項(xiàng)目封裝的實(shí)現(xiàn)方式各不相同崇决。
- (void)startUploadItem:(id)item name:(NSString *)name completion:(void(^)(BOOL success))completion
{
NSLog(@"錄音文件開(kāi)始上傳");
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底挫、啟動(dòng)時(shí)刪除錄音中斷文件的recording標(biāo)志
a恒傻、刪除recording標(biāo)志和結(jié)束時(shí)間
- 中斷錄音需要?jiǎng)h除不確定的結(jié)束時(shí)間
- 刪除錄制中的文件的
.recording
表示該文件已經(jīng)錄制完成
- (NSString *)deleteRecordingTagAndEndTimeWithFilePath:(NSString *)recorderFilePath
{
// 刪除recording Tag
......
// 刪除結(jié)束錄音時(shí)間
endRecordTime = [NSString stringWithFormat:@"_%@",endRecordTime];
NSString *deleteEndTimeFilePath = [deleteRecordingTagFilePath stringByReplacingOccurrencesOfString:endRecordTime withString:@""];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager moveItemAtPath:recorderFilePath toPath:deleteEndTimeFilePath error:nil];
return deleteEndTimeFilePath;
}
b、使所有中斷的文件變成錄音完成狀態(tài)的文件
- (void)convertRecordingFileToFinishedFile
{
NSLog(@"因?yàn)殇浺糁袛嘟ǖ耍瑢ecording標(biāo)志刪除掉盈厘,變成錄音完成的文件");
NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) firstObject];
NSString *recordFilePath = [libraryPath stringByAppendingString:@"/Caches/Recorder/"];
NSLog(@"錄音文件目錄路徑為:%@",recordFilePath);
NSFileManager *manager = [NSFileManager defaultManager];
// 獲得當(dāng)前文件的所有子文件:subpathsAtPath:
NSArray *pathList = [manager subpathsAtPath:recordFilePath];
// 遍歷這個(gè)文件夾下面的子文件,獲得因中斷等原因未自動(dòng)轉(zhuǎn)換成功的caf文件和mp3文件
for (NSString *audioPath in pathList)
{
if ([audioPath containsString:@"recording"])
{
// 每個(gè)正在錄音的文件的路徑
NSString *recordingFilePath = [recordFilePath stringByAppendingString:audioPath];
// 因?yàn)殇浺糁袛喙俦撸瑢ecording標(biāo)志刪除掉沸手,變成錄音完成的文件
// 同時(shí)刪除結(jié)束錄音時(shí)間
[self deleteRecordingTagAndEndTimeWithFilePath:recordingFilePath];
}
}
}
c、調(diào)用時(shí)機(jī)
該方法在APP啟動(dòng)的時(shí)候調(diào)用注簿,因?yàn)橹袛喽紩?huì)退出APP契吉,留下這些還在錄制中的音頻文件。
所以需要在啟動(dòng)APP的時(shí)候就將這些正處于錄制狀態(tài)中的遺留文件變?yōu)殇浿仆瓿蔂顟B(tài)的文件诡渴,之后在每次5分鐘的掃描時(shí)進(jìn)行轉(zhuǎn)化和上傳栅隐。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[UCARRecordSoundTool shareUCARRecordSoundTool] convertRecordingFileToFinishedFile];
}
2、將中斷文件轉(zhuǎn)化為UCAR文件
a玩徊、獲得因中斷等原因未自動(dòng)轉(zhuǎn)換成功的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
{
// 防空處理
.......
// 遍歷這個(gè)文件夾下面的子文件,獲得因中斷等原因未自動(dòng)轉(zhuǎn)換成功的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
文件轉(zhuǎn)化為mp3
文件后可能和之前邊錄制邊轉(zhuǎn)化的mp3
文件重了,導(dǎo)致在對(duì)兩個(gè)相同的mp3
文件進(jìn)行加密時(shí)畔塔,后加密的那個(gè)mp3
文件判斷為文件已經(jīng)存在潭辈,則報(bào)錯(cuò)。所以需要判斷caf
文件名稱和mp3
文件名稱是否相等澈吨,相等說(shuō)明是同一個(gè)錄音文件則直接刪除即可把敢。
- (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])// 相等說(shuō)明是同一個(gè)錄音文件則直接刪除即可
{
// 每個(gè)caf錄音文件的路徑
NSString *cafRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
[self deleteRecordFileWithFilePath:cafRecordFilePath];
}
else// 否則加入caf待轉(zhuǎn)錄列表
{
[cafAudioPathList addObject:audioPath];
}
}
}
}
}
NSLog(@"未成功生成mp3的剩余caf文件:%@",cafAudioPathList);
}
b、將獲得的caf文件和mp3文件進(jìn)行加密轉(zhuǎn)化為UCAR文件
將剩余caf
文件轉(zhuǎn)化為UCAR
谅辣。
- (NSArray *)convertAudioToUCARWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix sampleRate:(int)sampleRate
{
.......
// 存儲(chǔ)轉(zhuǎn)化而成的UCAR文件的數(shù)據(jù)用于上傳
NSMutableArray *UCARAudioPathList = [NSMutableArray array];
// 將剩余caf文件轉(zhuǎn)化為UCAR
if (cafAudioPathList.count > 0)
{
for (NSString *audioPath in cafAudioPathList)
{
// 每個(gè)caf錄音文件的路徑
NSString *cafRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
// 采樣率
[UCARLameTool shareUCARLameTool].sampleRate = sampleRate;
// 轉(zhuǎn)為MP3
[[UCARLameTool shareUCARLameTool] audioToMP3:cafRecordFilePath isDeleteSourchFile:YES withSuccessBack:^(NSString * _Nonnull resultPath) {
NSLog(@"轉(zhuǎn)為MP3后的路徑 = %@",resultPath);
// 將mp3文件進(jìn)行加密
NSString *encryptedRecorderDataWithFilePath = [self encryptedRecorderDataWithFilePath:resultPath encryptKey:encryptKey modifySuffix:modifySuffix];
if (encryptedRecorderDataWithFilePath && ![encryptedRecorderDataWithFilePath isEqualToString:@""])
{
[UCARAudioPathList addObject:encryptedRecorderDataWithFilePath];
}
} withFailBack:^(NSString * _Nonnull error) {
NSLog(@"將caf文件轉(zhuǎn)換為mp3文件失斝拊蕖:%@",error);
}];
}
}
.......
}
將剩余mp3
文件轉(zhuǎn)化為UCAR
。
- (NSArray *)convertAudioToUCARWithEncryptKey:(NSString *)encryptKey modifySuffix:(NSString *)modifySuffix sampleRate:(int)sampleRate
{
.......
if (mp3AudioPathList.count > 0)
{
for (NSString *audioPath in mp3AudioPathList)
{
// 每個(gè)mp3錄音文件的路徑
NSString *mp3RecordFilePath = [recordFilePath stringByAppendingString:audioPath];
// 將mp3文件進(jìn)行加密
NSString *encryptedRecorderDataWithFilePath = [self encryptedRecorderDataWithFilePath:mp3RecordFilePath encryptKey:encryptKey modifySuffix:modifySuffix];
if (encryptedRecorderDataWithFilePath && ![encryptedRecorderDataWithFilePath isEqualToString:@""])
{
[UCARAudioPathList addObject:encryptedRecorderDataWithFilePath];
}
}
}
.......
}
返回轉(zhuǎn)化而成的UCAR
文件的路徑列表用于上傳桑阶。
return [UCARAudioPathList copy];
c柏副、調(diào)用時(shí)機(jī)
該方法在APP啟動(dòng)的時(shí)候調(diào)用勾邦,因?yàn)橹袛喽紩?huì)退出APP,留下這些轉(zhuǎn)化失敗的音頻文件割择,所以啟動(dòng)的時(shí)候可以將其全部轉(zhuǎn)化為UCAR
文件眷篇。需要注意的是,一定要在調(diào)用了convertRecordingFileToFinishedFile
之后進(jìn)行調(diào)用荔泳。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[UCARRecordSoundTool shareUCARRecordSoundTool] convertRecordingFileToFinishedFile];
[[UCARRecordSoundTool shareUCARRecordSoundTool] convertAudioToUCARWithEncryptKey:@"" modifySuffix:@"" sampleRate:0];
return YES;
}
這些因?yàn)橹袛喽鴮?dǎo)致的錄音文件會(huì)沒(méi)有準(zhǔn)確的結(jié)束時(shí)間蕉饼,可以和正常完成錄音的音頻文件相區(qū)分。
3玛歌、判斷中斷類型
a椎椰、注冊(cè)音頻錄制中斷通知
- (AVAudioRecorder *)audioRecorder
{
__weak typeof(self) weakSelf = self;
if (!_audioRecorder)
{
// 注冊(cè)音頻錄制中斷通知
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(handleNotification:) name:AVAudioSessionInterruptionNotification object:nil];
......
}
return _audioRecorder;
}
b、接收錄制中斷事件通知沾鳄,并處理相關(guān)事件
監(jiān)聽(tīng)諸如系統(tǒng)來(lái)電慨飘,鬧鐘響鈴,F(xiàn)acetime……導(dǎo)致的音頻中斷終端事件译荞。
- (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(@"錄音被打斷……開(kāi)始");
break;
case AVAudioSessionInterruptionTypeEnded:
NSLog(@"錄音被打斷……結(jié)束");
break;
}
}
// 判斷中斷的音頻錄制是否可恢復(fù)錄制
if([allKeys containsObject:AVAudioSessionInterruptionOptionKey])
{
AVAudioSessionInterruptionOptions shouldResume = [[notification.userInfo valueForKey:AVAudioSessionInterruptionOptionKey] integerValue];
if(shouldResume)
{
NSLog(@"錄音被打斷……結(jié)束瓤的。可以恢復(fù)錄音了");
}
}
}
c吞歼、移除通知
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
九圈膏、內(nèi)存溢出覆蓋最早的錄音
1、計(jì)算錄音文件總的大小
a篙骡、獲取所有的音頻文件
計(jì)算的內(nèi)存是包括所有類型的音頻文件占用的空間大小的總和稽坤,而不只是加密后的UCAR
文件,所以需要將錄音完成狀態(tài)的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"
)
不能獲取還處于錄音狀態(tài)中的文件得湘,因?yàn)殇浺魻顟B(tài)中的文件的大小處于不停變化的狀態(tài)杖玲。
2020-11-23 10:21:45.840982+0800 Demo[83676:3085221] 獲得當(dāng)前文件的所有子文件:(
"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"
)
實(shí)現(xiàn)的代碼如下:
- (double)calculationRecordFileSizeSum
{
// 遍歷這個(gè)文件夾下面的子文件,只獲得錄音文件
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"];
// 通過(guò)對(duì)比文件的延展名(擴(kuò)展名淘正、尾綴)來(lái)區(qū)分是不是錄音文件
if (isUCAR || isFailMp3 || isFailCaf)
{
// 把篩選出來(lái)的文件放到數(shù)組中 -> 得到所有的音頻文件
[audioPathList addObject:audioPath];
}
}
}
NSLog(@"獲得當(dāng)前文件的所有子文件:%@",pathList);
NSLog(@"所有的音頻文件:%@",audioPathList);
......
}
b摆马、計(jì)算所有的音頻文件大小
2020-11-23 10:21:45.841421+0800 Demo[83676:3085221] 所有的音頻文件大小為:0.068947MB
通過(guò)獲取文件的大小屬性來(lái)進(jìn)行累加,再將其轉(zhuǎn)化為MB為單位鸿吆。
- (double)calculationRecordFileSizeSum
{
.......
double allRecordFileSize = 0;
for (NSString *audioPath in audioPathList)
{
// 每個(gè)錄音文件的路徑
NSString *everyRecordFilePath = [recordFilePath stringByAppendingString:audioPath];
// 每個(gè)錄音文件的大小
NSNumber *everyRecordFileSize = [manager attributesOfItemAtPath:everyRecordFilePath error:nil][NSFileSize];
// 所有錄音文件的大小
allRecordFileSize += [everyRecordFileSize doubleValue];
}
allRecordFileSize = allRecordFileSize / 1024.0 / 1024.0;
NSLog(@"所有的音頻文件大小為:%fMB",allRecordFileSize);
return allRecordFileSize;
}
2囤采、內(nèi)存溢出時(shí)自動(dòng)覆蓋最早的錄音文件
a、用于判斷的兩個(gè)條件
條件一:所有的音頻文件限制大小惩淳。錄音文件最大占用內(nèi)存大小蕉毯,以MB為單位,可配置,默認(rèn)1024MB恕刘,即1個(gè)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褐着、未溢出則直接返回
- 已經(jīng)錄制的所有的音頻文件大小如果小于音頻文件限制大小則表示還需要繼續(xù)錄制
- 如果大于系統(tǒng)可用存儲(chǔ)空間的最小值(暫定為100MB)則表示系統(tǒng)還允許繼續(xù)錄制
- (void)coverEarliestRecordFileWithMemoryLimit:(double)maximumMemory
{
// 所有的音頻文件大小
double allRecordFileSize = [self calculationRecordFileSizeSum];
if (allRecordFileSize < maximumMemory && [self getFreeDiskSpace] > 100)
{
return;
}
......
}
c、按照錄音文件的錄制開(kāi)始時(shí)間進(jìn)行升序排序
當(dāng)配置所有的音頻文件限制大小為0.1MB時(shí)候的輸出結(jié)果如下托呕。
[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",
......
這樣排序后的列表中的第一個(gè)文件就是最早錄制的音頻文件含蓉。
- (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;
// 獲取開(kāi)始時(shí)間 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、覆蓋最早的錄音项郊,其實(shí)就是刪除最早的錄音
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馅扣、調(diào)用時(shí)機(jī)
音頻文件在司機(jī)端本地最多占用1024M存儲(chǔ)空間,空間滿時(shí)自動(dòng)覆蓋生成時(shí)間最早的文件着降,空間上限可配置差油。
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
{
.......
[self coverEarliestRecordFileWithMemoryLimit:self.maximumMemory];
NSLog(@"錄音結(jié)束");
}
十、音頻知識(shí)
1任洞、音頻壓縮編碼格式
a蓄喇、壓縮需求
我們通常從音樂(lè)App(如:網(wǎng)易云音樂(lè))聽(tīng)歌時(shí),會(huì)看到一首歌需要的存儲(chǔ)空間大概是10M左右交掏,對(duì)于手機(jī)磁盤來(lái)說(shuō)這是可以接受的妆偏。但在網(wǎng)絡(luò)中實(shí)時(shí)在線傳播的話,這個(gè)數(shù)據(jù)量可能就太大了盅弛,所以必須對(duì)其進(jìn)行壓縮編碼钱骂。壓縮編碼的基本指標(biāo)之一就是壓縮比,壓縮比通常小于1(否則就沒(méi)有必要去做壓縮)挪鹏。
b见秽、壓縮算法
包含無(wú)損壓縮和有損壓縮,常用壓縮格式中讨盒,用的較多的是有損壓縮张吉。
無(wú)損壓縮:解壓后的數(shù)據(jù)可以完全復(fù)原。
有損壓縮:解壓后的數(shù)據(jù)不能完全復(fù)原催植,會(huì)丟失一部分信息肮蛹,壓縮比越小,丟失的信息就越多创南,信號(hào)還原后失真就會(huì)越大伦忠。
c、壓縮編碼原理
壓縮編碼原理實(shí)際上是壓縮掉冗余信號(hào)稿辙,冗余信號(hào)是指不能被人耳感知到的信號(hào)昆码,包含人耳聽(tīng)覺(jué)范圍之外的音頻信號(hào)以及被掩蔽掉的音頻信號(hào)。
d、常用壓縮編碼格式
PCM編碼
PCM
編碼是沒(méi)有壓縮的音頻數(shù)據(jù)赋咽,也可以叫音頻裸數(shù)據(jù)旧噪。音頻的裸數(shù)據(jù)格式就是脈沖編碼調(diào)制(Pulse Code Modulation
,PCM
)數(shù)據(jù)脓匿,是按照一定的格式記錄采樣和量化后的數(shù)字?jǐn)?shù)據(jù)淘钟,描述一段PCM
數(shù)據(jù)需要這幾個(gè)概念——量化格式(sampleFormat
)、采樣率(sampleRate
)陪毡、聲道數(shù)(channel
)米母。
采樣率是指自然界的音頻即聲波轉(zhuǎn)換為數(shù)字?jǐn)?shù)據(jù)保存時(shí)單位時(shí)間采樣個(gè)數(shù)。采樣率越高毡琉,精確度越大铁瞒。人對(duì)頻率的識(shí)別范圍是 20HZ - 20000HZ
。所以22050
的采樣頻率是常用的音頻采樣率慧耍,而44100
采樣率即是CD級(jí)別。16bit pcm
意味著使用兩個(gè)字節(jié)去保存采樣值丐谋。采樣數(shù)據(jù)記錄的是振幅芍碧,采樣精度取決于儲(chǔ)存空間的大小。2 字節(jié)(也就是16bit
) 65536個(gè)等級(jí) 笋鄙, CD級(jí)別师枣,16bit pcm
就是最常見(jiàn)的。
WAV編碼
WAV
編碼有多種實(shí)現(xiàn)方式萧落,但是都不會(huì)進(jìn)行壓縮操作践美。在PCM
數(shù)據(jù)格式的前面加上44字節(jié),分別用來(lái)描述PCM
的采樣率找岖、聲道數(shù)陨倡、數(shù)據(jù)格式等信息。
MP3編碼
MP3
具有不錯(cuò)的壓縮比许布,使用LAME
編碼(MP3
編碼格式的一種實(shí)現(xiàn))的中高碼率的MP3
文件兴革,聽(tīng)感上非常接近源WAV
文件。音質(zhì)在128Kbit/s
以上表現(xiàn)還不錯(cuò)蜜唾,壓縮比較高杂曲,兼容性好,用于音樂(lè)欣賞袁余。
AAC編碼
新一代的音頻有損壓縮技術(shù)擎勘,在小于128Kbit/s
的碼率下表現(xiàn)優(yōu)異,并且多用于視頻中音頻軌的編碼颖榜。
Ogg編碼
一種非常有潛力的編碼棚饵。Ogg
有著非常出色的算法煤裙,可以用更小的碼率達(dá)到更好的音質(zhì),128Kbit/s
的Ogg
比192Kbit/s
甚至更高碼率的MP3
還要出色噪漾。Ogg目前受支持的情況還不夠好硼砰,適用語(yǔ)音聊天。
2欣硼、lame靜態(tài)庫(kù)
a题翰、通訊格式
IM項(xiàng)目中涉及語(yǔ)音通訊,需要選擇一款優(yōu)良的通訊格式分别。由于iOS原生不支持錄制AMR
格式和MP3
格式遍愿,但是這兩個(gè)格式是目前移動(dòng)端比較喜愛(ài)的選擇存淫。最開(kāi)始傾向于AMR
語(yǔ)音通訊耘斩,因?yàn)?code>AMR體積很小,很省流量桅咆,但是PC端播放AMR
同樣需要轉(zhuǎn)碼括授,耗時(shí)且體驗(yàn)不好,最后選擇中庸但流行的MP3
作為語(yǔ)音通訊的橋梁岩饼。
b荚虚、編碼器
因?yàn)閕OS沒(méi)有原生錄制編碼為MP3
和轉(zhuǎn)碼MP3
的功能,需要三方庫(kù)支持籍茧,目前最成熟且最廣泛的轉(zhuǎn)碼庫(kù)為lame
庫(kù)版述。LAME
是一個(gè)開(kāi)源的MP3
音頻壓縮軟件,可以將音頻裸PCM
數(shù)據(jù)編碼成mp3
寞冯,目前是公認(rèn)有損品質(zhì)MP3
中壓縮效果最好的編碼器渴析。
AMR
和MP3
錄制與轉(zhuǎn)換都需要用到三方庫(kù),但MP3
還需要自己編譯并構(gòu)建Lame
靜態(tài)庫(kù)吮龄,于是在項(xiàng)目中引入了lame.h
文件和libmp3lame.a
靜態(tài)庫(kù)框架俭茧。
- 需要將
lame
打包轉(zhuǎn)化為可用于App
的靜態(tài)庫(kù)引入項(xiàng)目。 - 音頻處理:錄制時(shí)已經(jīng)采用最低端質(zhì)量錄制
AVAudioQualityMin
漓帚,采樣率為8000HZ母债,聲道數(shù)為單聲道。 - 利用
lame
庫(kù)將wav
或者caf
轉(zhuǎn)換mp3
后尝抖,體積有明顯減小毡们,音質(zhì)也能保證清晰流暢。
c昧辽、lame生成靜態(tài)庫(kù)
- 下載 lame 的最新版本并解壓到桌面的一個(gè)文件夾里例如
lame
衙熔。 - 為了把下載的
lame
生成靜態(tài)庫(kù),需要下載 build 的腳本奴迅,下載之后將得到lame-build.sh
拷貝到上一步解壓好的文件夾里青责。
- 使用文本編輯打開(kāi)
built-lame.sh
挺据,修改腳本為可執(zhí)行腳本并在終端執(zhí)行腳本。
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...
- 會(huì)生成支持多種架構(gòu)的
fat-lame
文件脖隶,簡(jiǎn)稱胖文件扁耐,把fat-lame
里面的lame.h
與libmp3lame.a
導(dǎo)入工程即可。
- 導(dǎo)入編譯完成后的靜態(tài)庫(kù)到工程产阱,使用代碼都寫在
LameTool
類里面婉称。
Demo
Demo在我的Github上,歡迎下載构蹬。
EnterpriseDemo