IOS企业:司机端APP行驶全程录音功能(下)

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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、加密失败的文件处理

加密失败通常是由于传入的recorderFilePathencryptKeymodifySuffix为空。

- (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中调用的,而此时录音器的参数都还没有配置,所以encryptKeymodifySuffix为空,就会导致崩溃。

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、开启上传录音任务
  1. 转换所有文件为待上传状态
  2. 获取待上传的文件
  3. 批量上传
- (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];
}
启动时删除录音中断文件的recording标志

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;
}

这些因为中断而导致的录音文件会没有准确的结束时间,可以和正常完成录音的音频文件相区分。

将获得的caf文件和mp3文件进行加密转化为UCAR文件

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文件,所以需要将录音完成状态的cafmp3UCAR所有文件全部获得。

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 ModulationPCM)数据,是按照一定的格式记录采样和量化后的数字数据,描述一段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/sOgg192Kbit/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中压缩效果最好的编码器。

AMRMP3录制与转换都需要用到三方库,但MP3还需要自己编译并构建Lame静态库,于是在项目中引入了lame.h文件和libmp3lame.a 静态库框架。

  1. 需要将lame打包转化为可用于App的静态库引入项目。
  2. 音频处理:录制时已经采用最低端质量录制 AVAudioQualityMin,采样率为8000HZ,声道数为单声道。
  3. 利用lame库将wav或者caf转换mp3后,体积有明显减小,音质也能保证清晰流畅。

c、lame生成静态库
  1. 下载 lame 的最新版本并解压到桌面的一个文件夹里例如 lame
  2. 为了把下载的lame生成静态库,需要下载 build 的脚本,下载之后将得到lame-build.sh拷贝到上一步解压好的文件夹里。
lame生成静态库
  1. 使用文本编辑打开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...
  1. 会生成支持多种架构的fat-lame文件,简称胖文件,把fat-lame里面的 lame.hlibmp3lame.a 导入工程即可。
fat-lame
  1. 导入编译完成后的静态库到工程,使用代码都写在 LameTool 类里面。

Demo

Demo在我的Github上,欢迎下载。
EnterpriseDemo

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 应用场景 在即时通讯APP中,例如微信,QQ,等都有语音发送功能,一般都要先将录音录制下来才能发送录音。 音频相关...
    翀鹰精灵阅读 27,638评论 20 44
  • 1. 导入录音功能依赖的框架 2. 在相应界面引入头文件 然后遵守协议AVAudioRecorderDelegat...
    黑白灰的绿i阅读 6,460评论 6 13
  • 简介 最近公司研发了一个语音识别的框架,但这个框架是后端识别,所以需要手机端录音,录音后将音频文件通转成NSDat...
    iOS弗森科阅读 2,908评论 1 7
  • ps:文章内容的代码部分,由于不便暴露业务逻辑,可能会有部分删减,但是主体功能基本保留 背景 这段时间应公司业务需...
    赛萧何阅读 7,452评论 2 9
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,523评论 16 22