IOS企業(yè):司機(jī)端APP行駛?cè)啼浺艄δ埽ㄏ拢?/h1>

原創(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秒鼻听。

進(jìn)行加密
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阳仔、加密失敗的文件處理

加密失敗通常是由于傳入的recorderFilePathencryptKey扣泊、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è)試用阱飘。

進(jìn)行解密
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)有配置高帖,所以encryptKeymodifySuffix為空缰儿,就會(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ù)
  1. 轉(zhuǎn)換所有文件為待上傳狀態(tài)
  2. 獲取待上傳的文件
  3. 批量上傳
- (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];
}
啟動(dòng)時(shí)刪除錄音中斷文件的recording標(biāo)志

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ū)分。

將獲得的caf文件和mp3文件進(jìn)行加密轉(zhuǎn)化為UCAR文件

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 ModulationPCM)數(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/sOgg192Kbit/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中壓縮效果最好的編碼器渴析。

AMRMP3錄制與轉(zhuǎn)換都需要用到三方庫(kù),但MP3還需要自己編譯并構(gòu)建Lame靜態(tài)庫(kù)吮龄,于是在項(xiàng)目中引入了lame.h文件和libmp3lame.a 靜態(tài)庫(kù)框架俭茧。

  1. 需要將lame打包轉(zhuǎn)化為可用于App的靜態(tài)庫(kù)引入項(xiàng)目。
  2. 音頻處理:錄制時(shí)已經(jīng)采用最低端質(zhì)量錄制 AVAudioQualityMin漓帚,采樣率為8000HZ母债,聲道數(shù)為單聲道。
  3. 利用lame庫(kù)將wav或者caf轉(zhuǎn)換mp3后尝抖,體積有明顯減小毡们,音質(zhì)也能保證清晰流暢。

c昧辽、lame生成靜態(tài)庫(kù)
  1. 下載 lame 的最新版本并解壓到桌面的一個(gè)文件夾里例如 lame衙熔。
  2. 為了把下載的lame生成靜態(tài)庫(kù),需要下載 build 的腳本奴迅,下載之后將得到lame-build.sh拷貝到上一步解壓好的文件夾里青责。
lame生成靜態(tài)庫(kù)
  1. 使用文本編輯打開(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...
  1. 會(huì)生成支持多種架構(gòu)的fat-lame文件脖隶,簡(jiǎn)稱胖文件扁耐,把fat-lame里面的 lame.hlibmp3lame.a 導(dǎo)入工程即可。
fat-lame
  1. 導(dǎo)入編譯完成后的靜態(tài)庫(kù)到工程产阱,使用代碼都寫在 LameTool 類里面婉称。

Demo

Demo在我的Github上,歡迎下載构蹬。
EnterpriseDemo

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載王暗,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

  • 序言:七十年代末庄敛,一起剝皮案震驚了整個(gè)濱河市俗壹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌藻烤,老刑警劉巖绷雏,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異怖亭,居然都是意外死亡涎显,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門兴猩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)期吓,“玉大人,你說(shuō)我怎么就攤上這事倾芝√智冢” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵蛀醉,是天一觀的道長(zhǎng)悬襟。 經(jīng)常有香客問(wèn)我,道長(zhǎng)拯刁,這世上最難降的妖魔是什么脊岳? 我笑而不...
    開(kāi)封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮垛玻,結(jié)果婚禮上割捅,老公的妹妹穿的比我還像新娘。我一直安慰自己帚桩,他們只是感情好亿驾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著账嚎,像睡著了一般莫瞬。 火紅的嫁衣襯著肌膚如雪儡蔓。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天疼邀,我揣著相機(jī)與錄音喂江,去河邊找鬼。 笑死旁振,一個(gè)胖子當(dāng)著我的面吹牛获询,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拐袜,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吉嚣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蹬铺?” 一聲冷哼從身側(cè)響起尝哆,我...
    開(kāi)封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丛塌,沒(méi)想到半個(gè)月后较解,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體畜疾,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赴邻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了啡捶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姥敛。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瞎暑,靈堂內(nèi)的尸體忽然破棺而出彤敛,到底是詐尸還是另有隱情,我是刑警寧澤了赌,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布墨榄,位于F島的核電站,受9級(jí)特大地震影響勿她,放射性物質(zhì)發(fā)生泄漏袄秩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一逢并、第九天 我趴在偏房一處隱蔽的房頂上張望之剧。 院中可真熱鬧,春花似錦砍聊、人聲如沸背稼。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蟹肘。三九已至词疼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帘腹,已是汗流浹背寒跳。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留竹椒,地道東北人童太。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像胸完,于是被迫代替她去往敵國(guó)和親书释。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 應(yīng)用場(chǎng)景 在即時(shí)通訊APP中赊窥,例如微信爆惧,QQ,等都有語(yǔ)音發(fā)送功能锨能,一般都要先將錄音錄制下來(lái)才能發(fā)送錄音扯再。 音頻相關(guān)...
    翀鷹精靈閱讀 27,644評(píng)論 20 44
  • 1. 導(dǎo)入錄音功能依賴的框架 2. 在相應(yīng)界面引入頭文件 然后遵守協(xié)議AVAudioRecorderDelegat...
    黑白灰的綠i閱讀 6,463評(píng)論 6 13
  • 簡(jiǎn)介 最近公司研發(fā)了一個(gè)語(yǔ)音識(shí)別的框架,但這個(gè)框架是后端識(shí)別址遇,所以需要手機(jī)端錄音熄阻,錄音后將音頻文件通轉(zhuǎn)成NSDat...
    iOS弗森科閱讀 2,911評(píng)論 1 7
  • ps:文章內(nèi)容的代碼部分,由于不便暴露業(yè)務(wù)邏輯倔约,可能會(huì)有部分刪減秃殉,但是主體功能基本保留 背景 這段時(shí)間應(yīng)公司業(yè)務(wù)需...
    賽蕭何閱讀 7,458評(píng)論 2 9
  • 久違的晴天,家長(zhǎng)會(huì)浸剩。 家長(zhǎng)大會(huì)開(kāi)好到教室時(shí)钾军,離放學(xué)已經(jīng)沒(méi)多少時(shí)間了。班主任說(shuō)已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)绢要。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,523評(píng)論 16 22