TUIAudioRecorder.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. // Created by Tencent on 2023/06/09.
  2. // Copyright © 2023 Tencent. All rights reserved.
  3. //
  4. // TUIAudioRecorder.m
  5. // TUIChat
  6. //
  7. #import "TUIAudioRecorder.h"
  8. #import <AVFoundation/AVFoundation.h>
  9. #import <TIMCommon/TIMCommonModel.h>
  10. #import <TIMCommon/TIMDefine.h>
  11. #import <TUICore/TUICore.h>
  12. #import <TUICore/TUILogin.h>
  13. #import "TUIAIDenoiseSignatureManager.h"
  14. @interface TUIAudioRecorder () <AVAudioRecorderDelegate, TUINotificationProtocol>
  15. @property(nonatomic, strong) AVAudioRecorder *recorder;
  16. @property(nonatomic, strong) NSTimer *recordTimer;
  17. @property(nonatomic, assign) BOOL isUsingCallKitRecorder;
  18. @property(nonatomic, copy, readwrite) NSString *recordedFilePath;
  19. @property(nonatomic, assign) NSTimeInterval currentRecordTime;
  20. @end
  21. @implementation TUIAudioRecorder
  22. - (instancetype)init {
  23. self = [super init];
  24. if (self) {
  25. [self configNotify];
  26. }
  27. return self;
  28. }
  29. - (void)configNotify {
  30. [TUICore registerEvent:TUICore_RecordAudioMessageNotify subKey:TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey object:self];
  31. }
  32. - (void)dealloc {
  33. [TUICore unRegisterEventByObject:self];
  34. }
  35. #pragma mark - Public
  36. - (void)record {
  37. [self checkMicPermissionWithCompletion:^(BOOL isGranted, BOOL isFirstChek) {
  38. if (TUILogin.getCurrentBusinessScene != None) {
  39. [TUITool makeToast:TIMCommonLocalizableString(TUIKitMessageTypeOtherUseMic) duration:3];
  40. return;
  41. }
  42. if (isFirstChek) {
  43. if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didCheckPermission:isFirstTime:)]) {
  44. [self.delegate audioRecorder:self didCheckPermission:isGranted isFirstTime:YES];
  45. }
  46. return;
  47. }
  48. if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didCheckPermission:isFirstTime:)]) {
  49. [self.delegate audioRecorder:self didCheckPermission:isGranted isFirstTime:NO];
  50. }
  51. if (isGranted) {
  52. [self createRecordedFilePath];
  53. if (![self startCallKitRecording]) {
  54. [self startSystemRecording];
  55. }
  56. }
  57. }];
  58. }
  59. - (void)stop {
  60. [self stopRecordTimer];
  61. if (self.isUsingCallKitRecorder) {
  62. [self stopCallKitRecording];
  63. } else {
  64. [self stopSystemRecording];
  65. }
  66. }
  67. - (void)cancel {
  68. [self stopRecordTimer];
  69. if (self.isUsingCallKitRecorder) {
  70. [self stopCallKitRecording];
  71. } else {
  72. [self cancelSystemRecording];
  73. }
  74. }
  75. #pragma mark - Private
  76. - (void)createRecordedFilePath {
  77. self.recordedFilePath = [TUIKit_Voice_Path stringByAppendingString:[TUITool genVoiceName:nil withExtension:@"m4a"]];
  78. }
  79. - (void)stopRecordTimer {
  80. if (self.recordTimer) {
  81. [self.recordTimer invalidate];
  82. self.recordTimer = nil;
  83. }
  84. }
  85. #pragma mark-- Timer
  86. - (void)triggerRecordTimer {
  87. self.currentRecordTime = 0;
  88. self.recordTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(onRecordTimerTriggered:) userInfo:nil repeats:YES];
  89. }
  90. - (void)onRecordTimerTriggered:(NSTimer *)timer {
  91. [self.recorder updateMeters];
  92. if (self.isUsingCallKitRecorder) {
  93. /// To ensure the callkit recorder's recording time is enough for 60 seconds.
  94. self.currentRecordTime += 0.2;
  95. if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didRecordTimeChanged:)]) {
  96. [self.delegate audioRecorder:self didRecordTimeChanged:self.currentRecordTime];
  97. }
  98. } else {
  99. float power = [self.recorder averagePowerForChannel:0];
  100. NSTimeInterval currentTime = self.recorder.currentTime;
  101. if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didPowerChanged:)]) {
  102. [self.delegate audioRecorder:self didPowerChanged:power];
  103. }
  104. if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didRecordTimeChanged:)]) {
  105. [self.delegate audioRecorder:self didRecordTimeChanged:currentTime];
  106. }
  107. }
  108. }
  109. - (void)checkMicPermissionWithCompletion:(void (^)(BOOL isGranted, BOOL isFirstChek))completion {
  110. AVAudioSessionRecordPermission permission = AVAudioSession.sharedInstance.recordPermission;
  111. /**
  112. * For the first request for authorization after a new installation, it is necessary to
  113. * determine whether it is Undetermined again to avoid errors.
  114. */
  115. if (permission == AVAudioSessionRecordPermissionDenied || permission == AVAudioSessionRecordPermissionUndetermined) {
  116. [AVAudioSession.sharedInstance requestRecordPermission:^(BOOL granted) {
  117. dispatch_async(dispatch_get_main_queue(), ^{
  118. if (completion) {
  119. completion(granted, YES);
  120. }
  121. });
  122. }];
  123. return;
  124. }
  125. BOOL isGranted = permission == AVAudioSessionRecordPermissionGranted;
  126. if (completion) {
  127. completion(isGranted, NO);
  128. }
  129. }
  130. #pragma mark-- Record audio using system framework
  131. - (void)startSystemRecording {
  132. self.isUsingCallKitRecorder = NO;
  133. AVAudioSession *session = [AVAudioSession sharedInstance];
  134. NSError *error = nil;
  135. [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
  136. [session setActive:YES error:&error];
  137. NSDictionary *recordSetting = [[NSDictionary alloc] initWithObjectsAndKeys:
  138. /**
  139. * Sampling rate: 8000/11025/22050/44100/96000 (this parameter affects the audio
  140. * quality)
  141. */
  142. [NSNumber numberWithFloat:16000.0], AVSampleRateKey,
  143. /**
  144. * Audio format
  145. */
  146. [NSNumber numberWithInt:kAudioFormatMPEG4AAC], AVFormatIDKey,
  147. /**
  148. * Sampling bits: 8, 16, 24, 32, default is 16
  149. */
  150. [NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
  151. /**
  152. * Number of audio channels 1 or 2
  153. */
  154. [NSNumber numberWithInt:1], AVNumberOfChannelsKey,
  155. /**
  156. * Recording quality
  157. */
  158. [NSNumber numberWithInt:AVAudioQualityHigh], AVEncoderAudioQualityKey, nil];
  159. [self createRecordedFilePath];
  160. NSURL *url = [NSURL fileURLWithPath:self.recordedFilePath];
  161. self.recorder = [[AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:nil];
  162. self.recorder.meteringEnabled = YES;
  163. [self.recorder prepareToRecord];
  164. [self.recorder record];
  165. [self.recorder updateMeters];
  166. [self triggerRecordTimer];
  167. NSLog(@"start system recording");
  168. }
  169. - (void)stopSystemRecording {
  170. if (AVAudioSession.sharedInstance.recordPermission == AVAudioSessionRecordPermissionDenied) {
  171. return;
  172. }
  173. if ([self.recorder isRecording]) {
  174. [self.recorder stop];
  175. }
  176. self.recorder = nil;
  177. NSLog(@"stop system recording");
  178. }
  179. - (void)cancelSystemRecording {
  180. if ([self.recorder isRecording]) {
  181. [self.recorder stop];
  182. }
  183. NSString *path = self.recorder.url.path;
  184. if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
  185. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  186. }
  187. self.recorder = nil;
  188. NSLog(@"cancel system recording");
  189. }
  190. #pragma mark-- Record audio using TUICallKit framework
  191. - (BOOL)startCallKitRecording {
  192. if (![TUICore getService:TUICore_TUIAudioMessageRecordService]) {
  193. NSLog(@"TUICallKit audio recording service does not exist");
  194. return NO;
  195. }
  196. NSString *signature = [TUIAIDenoiseSignatureManager sharedInstance].signature;
  197. if (signature.length == 0) {
  198. NSLog(@"denoise signature is empty");
  199. return NO;
  200. }
  201. NSMutableDictionary *audioRecordParam = [[NSMutableDictionary alloc] init];
  202. [audioRecordParam setValue:signature forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SignatureKey];
  203. [audioRecordParam setValue:@([TUILogin getSdkAppID]) forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SdkappidKey];
  204. [audioRecordParam setValue:self.recordedFilePath forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_PathKey];
  205. @weakify(self);
  206. void (^startCallBack)(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) =
  207. ^(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) {
  208. @strongify(self);
  209. NSString *method = param[@"method"];
  210. if ([method isEqualToString:TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey]) {
  211. [self onTUICallKitRecordStarted:errorCode];
  212. }
  213. };
  214. [TUICore callService:TUICore_TUIAudioMessageRecordService
  215. method:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod
  216. param:audioRecordParam
  217. resultCallback:startCallBack];
  218. self.isUsingCallKitRecorder = YES;
  219. NSLog(@"start TUICallKit recording");
  220. return true;
  221. }
  222. - (void)stopCallKitRecording {
  223. @weakify(self);
  224. void (^stopCallBack)(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) =
  225. ^(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) {
  226. @strongify(self);
  227. NSString *method = param[@"method"];
  228. if ([method isEqualToString:TUICore_RecordAudioMessageNotify_StopRecordAudioMessageSubKey]) {
  229. [self onTUICallKitRecordCompleted:errorCode];
  230. }
  231. };
  232. [TUICore callService:TUICore_TUIAudioMessageRecordService
  233. method:TUICore_TUIAudioMessageRecordService_StopRecordAudioMessageMethod
  234. param:nil
  235. resultCallback:stopCallBack];
  236. NSLog(@"stop TUICallKit recording");
  237. }
  238. #pragma mark - TUINotificationProtocol
  239. - (void)onNotifyEvent:(NSString *)key subKey:(NSString *)subKey object:(nullable id)anObject param:(NSDictionary *)param {
  240. if ([key isEqualToString:TUICore_RecordAudioMessageNotify]) {
  241. if (param == nil) {
  242. NSLog(@"TUICallKit notify param is invalid");
  243. return;
  244. }
  245. if ([subKey isEqualToString:TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey]) {
  246. NSUInteger volume = [param[@"volume"] unsignedIntegerValue];
  247. [self onTUICallKitVolumeChanged:volume];
  248. }
  249. }
  250. }
  251. - (void)onTUICallKitRecordStarted:(NSInteger)errorCode {
  252. switch (errorCode) {
  253. case TUICore_RecordAudioMessageNotifyError_None: {
  254. [self triggerRecordTimer];
  255. break;
  256. }
  257. case TUICore_RecordAudioMessageNotifyError_MicPermissionRefused: {
  258. break;
  259. }
  260. case TUICore_RecordAudioMessageNotifyError_StatusInCall: {
  261. [TUITool makeToast:TIMCommonLocalizableString(TUIKitInputRecordRejectedInCall)];
  262. break;
  263. }
  264. case TUICore_RecordAudioMessageNotifyError_StatusIsAudioRecording: {
  265. [TUITool makeToast:TIMCommonLocalizableString(TUIKitInputRecordRejectedIsRecording)];
  266. break;
  267. }
  268. case TUICore_RecordAudioMessageNotifyError_RequestAudioFocusFailed:
  269. case TUICore_RecordAudioMessageNotifyError_RecordInitFailed:
  270. case TUICore_RecordAudioMessageNotifyError_PathFormatNotSupport:
  271. case TUICore_RecordAudioMessageNotifyError_MicStartFail:
  272. case TUICore_RecordAudioMessageNotifyError_MicNotAuthorized:
  273. case TUICore_RecordAudioMessageNotifyError_MicSetParamFail:
  274. case TUICore_RecordAudioMessageNotifyError_MicOccupy: {
  275. [self stopCallKitRecording];
  276. NSLog(@"start TUICallKit recording failed, errorCode: %ld", (long)errorCode);
  277. break;
  278. }
  279. case TUICore_RecordAudioMessageNotifyError_InvalidParam:
  280. case TUICore_RecordAudioMessageNotifyError_SignatureError:
  281. case TUICore_RecordAudioMessageNotifyError_SignatureExpired:
  282. default: {
  283. [self stopCallKitRecording];
  284. [self startSystemRecording];
  285. NSLog(@"start TUICallKit recording failed, errorCode: %ld, switch to system recorder", (long)errorCode);
  286. break;
  287. }
  288. }
  289. }
  290. - (void)onTUICallKitRecordCompleted:(NSInteger)errorCode {
  291. switch (errorCode) {
  292. case TUICore_RecordAudioMessageNotifyError_None: {
  293. [self stopRecordTimer];
  294. break;
  295. }
  296. case TUICore_RecordAudioMessageNotifyError_NoMessageToRecord:
  297. case TUICore_RecordAudioMessageNotifyError_RecordFailed: {
  298. NSLog(@"stop TUICallKit recording failed, errorCode: %ld", (long)errorCode);
  299. }
  300. default:
  301. break;
  302. }
  303. }
  304. - (void)onTUICallKitVolumeChanged:(NSUInteger)volume {
  305. /// Adapt volume to power.
  306. float power = (NSInteger)volume - 90;
  307. if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didPowerChanged:)]) {
  308. [self.delegate audioRecorder:self didPowerChanged:power];
  309. }
  310. }
  311. @end