// Created by Tencent on 2023/06/09. // Copyright © 2023 Tencent. All rights reserved. // // TUIAudioRecorder.m // TUIChat // #import "TUIAudioRecorder.h" #import #import #import #import #import #import "TUIAIDenoiseSignatureManager.h" @interface TUIAudioRecorder () @property(nonatomic, strong) AVAudioRecorder *recorder; @property(nonatomic, strong) NSTimer *recordTimer; @property(nonatomic, assign) BOOL isUsingCallKitRecorder; @property(nonatomic, copy, readwrite) NSString *recordedFilePath; @property(nonatomic, assign) NSTimeInterval currentRecordTime; @end @implementation TUIAudioRecorder - (instancetype)init { self = [super init]; if (self) { [self configNotify]; } return self; } - (void)configNotify { [TUICore registerEvent:TUICore_RecordAudioMessageNotify subKey:TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey object:self]; } - (void)dealloc { [TUICore unRegisterEventByObject:self]; } #pragma mark - Public - (void)record { [self checkMicPermissionWithCompletion:^(BOOL isGranted, BOOL isFirstChek) { if (TUILogin.getCurrentBusinessScene != None) { [TUITool makeToast:TIMCommonLocalizableString(TUIKitMessageTypeOtherUseMic) duration:3]; return; } if (isFirstChek) { if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didCheckPermission:isFirstTime:)]) { [self.delegate audioRecorder:self didCheckPermission:isGranted isFirstTime:YES]; } return; } if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didCheckPermission:isFirstTime:)]) { [self.delegate audioRecorder:self didCheckPermission:isGranted isFirstTime:NO]; } if (isGranted) { [self createRecordedFilePath]; if (![self startCallKitRecording]) { [self startSystemRecording]; } } }]; } - (void)stop { [self stopRecordTimer]; if (self.isUsingCallKitRecorder) { [self stopCallKitRecording]; } else { [self stopSystemRecording]; } } - (void)cancel { [self stopRecordTimer]; if (self.isUsingCallKitRecorder) { [self stopCallKitRecording]; } else { [self cancelSystemRecording]; } } #pragma mark - Private - (void)createRecordedFilePath { self.recordedFilePath = [TUIKit_Voice_Path stringByAppendingString:[TUITool genVoiceName:nil withExtension:@"m4a"]]; } - (void)stopRecordTimer { if (self.recordTimer) { [self.recordTimer invalidate]; self.recordTimer = nil; } } #pragma mark-- Timer - (void)triggerRecordTimer { self.currentRecordTime = 0; self.recordTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(onRecordTimerTriggered:) userInfo:nil repeats:YES]; } - (void)onRecordTimerTriggered:(NSTimer *)timer { [self.recorder updateMeters]; if (self.isUsingCallKitRecorder) { /// To ensure the callkit recorder's recording time is enough for 60 seconds. self.currentRecordTime += 0.2; if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didRecordTimeChanged:)]) { [self.delegate audioRecorder:self didRecordTimeChanged:self.currentRecordTime]; } } else { float power = [self.recorder averagePowerForChannel:0]; NSTimeInterval currentTime = self.recorder.currentTime; if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didPowerChanged:)]) { [self.delegate audioRecorder:self didPowerChanged:power]; } if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didRecordTimeChanged:)]) { [self.delegate audioRecorder:self didRecordTimeChanged:currentTime]; } } } - (void)checkMicPermissionWithCompletion:(void (^)(BOOL isGranted, BOOL isFirstChek))completion { AVAudioSessionRecordPermission permission = AVAudioSession.sharedInstance.recordPermission; /** * For the first request for authorization after a new installation, it is necessary to * determine whether it is Undetermined again to avoid errors. */ if (permission == AVAudioSessionRecordPermissionDenied || permission == AVAudioSessionRecordPermissionUndetermined) { [AVAudioSession.sharedInstance requestRecordPermission:^(BOOL granted) { dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(granted, YES); } }); }]; return; } BOOL isGranted = permission == AVAudioSessionRecordPermissionGranted; if (completion) { completion(isGranted, NO); } } #pragma mark-- Record audio using system framework - (void)startSystemRecording { self.isUsingCallKitRecorder = NO; AVAudioSession *session = [AVAudioSession sharedInstance]; NSError *error = nil; [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; [session setActive:YES error:&error]; NSDictionary *recordSetting = [[NSDictionary alloc] initWithObjectsAndKeys: /** * Sampling rate: 8000/11025/22050/44100/96000 (this parameter affects the audio * quality) */ [NSNumber numberWithFloat:16000.0], AVSampleRateKey, /** * Audio format */ [NSNumber numberWithInt:kAudioFormatMPEG4AAC], AVFormatIDKey, /** * Sampling bits: 8, 16, 24, 32, default is 16 */ [NSNumber numberWithInt:16], AVLinearPCMBitDepthKey, /** * Number of audio channels 1 or 2 */ [NSNumber numberWithInt:1], AVNumberOfChannelsKey, /** * Recording quality */ [NSNumber numberWithInt:AVAudioQualityHigh], AVEncoderAudioQualityKey, nil]; [self createRecordedFilePath]; NSURL *url = [NSURL fileURLWithPath:self.recordedFilePath]; self.recorder = [[AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:nil]; self.recorder.meteringEnabled = YES; [self.recorder prepareToRecord]; [self.recorder record]; [self.recorder updateMeters]; [self triggerRecordTimer]; NSLog(@"start system recording"); } - (void)stopSystemRecording { if (AVAudioSession.sharedInstance.recordPermission == AVAudioSessionRecordPermissionDenied) { return; } if ([self.recorder isRecording]) { [self.recorder stop]; } self.recorder = nil; NSLog(@"stop system recording"); } - (void)cancelSystemRecording { if ([self.recorder isRecording]) { [self.recorder stop]; } NSString *path = self.recorder.url.path; if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; } self.recorder = nil; NSLog(@"cancel system recording"); } #pragma mark-- Record audio using TUICallKit framework - (BOOL)startCallKitRecording { if (![TUICore getService:TUICore_TUIAudioMessageRecordService]) { NSLog(@"TUICallKit audio recording service does not exist"); return NO; } NSString *signature = [TUIAIDenoiseSignatureManager sharedInstance].signature; if (signature.length == 0) { NSLog(@"denoise signature is empty"); return NO; } NSMutableDictionary *audioRecordParam = [[NSMutableDictionary alloc] init]; [audioRecordParam setValue:signature forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SignatureKey]; [audioRecordParam setValue:@([TUILogin getSdkAppID]) forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SdkappidKey]; [audioRecordParam setValue:self.recordedFilePath forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_PathKey]; @weakify(self); void (^startCallBack)(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) = ^(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) { @strongify(self); NSString *method = param[@"method"]; if ([method isEqualToString:TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey]) { [self onTUICallKitRecordStarted:errorCode]; } }; [TUICore callService:TUICore_TUIAudioMessageRecordService method:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod param:audioRecordParam resultCallback:startCallBack]; self.isUsingCallKitRecorder = YES; NSLog(@"start TUICallKit recording"); return true; } - (void)stopCallKitRecording { @weakify(self); void (^stopCallBack)(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) = ^(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) { @strongify(self); NSString *method = param[@"method"]; if ([method isEqualToString:TUICore_RecordAudioMessageNotify_StopRecordAudioMessageSubKey]) { [self onTUICallKitRecordCompleted:errorCode]; } }; [TUICore callService:TUICore_TUIAudioMessageRecordService method:TUICore_TUIAudioMessageRecordService_StopRecordAudioMessageMethod param:nil resultCallback:stopCallBack]; NSLog(@"stop TUICallKit recording"); } #pragma mark - TUINotificationProtocol - (void)onNotifyEvent:(NSString *)key subKey:(NSString *)subKey object:(nullable id)anObject param:(NSDictionary *)param { if ([key isEqualToString:TUICore_RecordAudioMessageNotify]) { if (param == nil) { NSLog(@"TUICallKit notify param is invalid"); return; } if ([subKey isEqualToString:TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey]) { NSUInteger volume = [param[@"volume"] unsignedIntegerValue]; [self onTUICallKitVolumeChanged:volume]; } } } - (void)onTUICallKitRecordStarted:(NSInteger)errorCode { switch (errorCode) { case TUICore_RecordAudioMessageNotifyError_None: { [self triggerRecordTimer]; break; } case TUICore_RecordAudioMessageNotifyError_MicPermissionRefused: { break; } case TUICore_RecordAudioMessageNotifyError_StatusInCall: { [TUITool makeToast:TIMCommonLocalizableString(TUIKitInputRecordRejectedInCall)]; break; } case TUICore_RecordAudioMessageNotifyError_StatusIsAudioRecording: { [TUITool makeToast:TIMCommonLocalizableString(TUIKitInputRecordRejectedIsRecording)]; break; } case TUICore_RecordAudioMessageNotifyError_RequestAudioFocusFailed: case TUICore_RecordAudioMessageNotifyError_RecordInitFailed: case TUICore_RecordAudioMessageNotifyError_PathFormatNotSupport: case TUICore_RecordAudioMessageNotifyError_MicStartFail: case TUICore_RecordAudioMessageNotifyError_MicNotAuthorized: case TUICore_RecordAudioMessageNotifyError_MicSetParamFail: case TUICore_RecordAudioMessageNotifyError_MicOccupy: { [self stopCallKitRecording]; NSLog(@"start TUICallKit recording failed, errorCode: %ld", (long)errorCode); break; } case TUICore_RecordAudioMessageNotifyError_InvalidParam: case TUICore_RecordAudioMessageNotifyError_SignatureError: case TUICore_RecordAudioMessageNotifyError_SignatureExpired: default: { [self stopCallKitRecording]; [self startSystemRecording]; NSLog(@"start TUICallKit recording failed, errorCode: %ld, switch to system recorder", (long)errorCode); break; } } } - (void)onTUICallKitRecordCompleted:(NSInteger)errorCode { switch (errorCode) { case TUICore_RecordAudioMessageNotifyError_None: { [self stopRecordTimer]; break; } case TUICore_RecordAudioMessageNotifyError_NoMessageToRecord: case TUICore_RecordAudioMessageNotifyError_RecordFailed: { NSLog(@"stop TUICallKit recording failed, errorCode: %ld", (long)errorCode); } default: break; } } - (void)onTUICallKitVolumeChanged:(NSUInteger)volume { /// Adapt volume to power. float power = (NSInteger)volume - 90; if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didPowerChanged:)]) { [self.delegate audioRecorder:self didPowerChanged:power]; } } @end