// // QXAudioRecorderView.m // QXLive // // Created by 启星 on 2025/11/13. // #import "QXAudioRecorderView.h" #import #import "UIButton+QX.h" #import "QXCOSUploadManager.h" #import "QXMineNetwork.h" @interface QXAudioRecorderView() // UI Components @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIView *recordContentView; @property (nonatomic, strong) UIImageView *recordButton; @property (nonatomic, strong) CAShapeLayer *progressLayer; @property (nonatomic, strong) UILabel *timeLabel; @property (nonatomic, strong) UILabel *stateLabel; @property (nonatomic, strong) UIButton *playButton; @property (nonatomic, strong) UIButton *resetButton; @property (nonatomic, strong) UIButton *authBtn; // Audio Components @property (nonatomic, strong) AVAudioRecorder *audioRecorder; @property (nonatomic, strong) AVAudioPlayer *audioPlayer; @property (nonatomic, strong) NSURL *originalAudioFileURL; @property (nonatomic, strong) NSURL *mp3AudioFileURL; @property (nonatomic, strong) NSURL *tempAudioFileURL; // 临时拼接文件 // Recording State @property (nonatomic, assign) AudioRecorderState state; @property (nonatomic, assign) NSTimeInterval currentDuration; // 当前段时长 @property (nonatomic, assign) NSTimeInterval totalDuration; // 总时长 @property (nonatomic, assign) NSInteger recordingSessionCount; // 录制段数 @property (nonatomic, strong) NSTimer *recordingTimer; @property (nonatomic, assign) BOOL isCancelled; @property (nonatomic, strong) NSMutableArray *recordedSegments; // 录制的音频段 @end @implementation QXAudioRecorderView - (instancetype)initWithFrame:(CGRect)frame { return [self initWithFrame:frame maxDuration:300.0]; } - (instancetype)initWithFrame:(CGRect)frame maxDuration:(NSTimeInterval)maxDuration { self = [super initWithFrame:frame]; if (self) { _maxDuration = maxDuration; _showPlaybackButton = YES; _state = AudioRecorderStateReady; _recordedSegments = [NSMutableArray array]; _recordingSessionCount = 0; [self setupUI]; [self setupAudioSession]; [self setupGestureRecognizers]; } return self; } - (void)setupUI { self.backgroundColor = [UIColor whiteColor]; [self addRoundedCornersWithRadius:16 byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)]; self.authBtn = [[UIButton alloc] initWithFrame:CGRectMake(SCREEN_WIDTH-ScaleWidth(57)-16, 16, ScaleWidth(57), ScaleWidth(20))]; [self.authBtn setImage:[UIImage imageNamed:@"singer_auth_btn"] forState:(UIControlStateNormal)]; [self.authBtn addTarget:self action:@selector(authAction) forControlEvents:(UIControlEventTouchUpInside)]; [self addSubview:self.authBtn]; self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake((SCREEN_WIDTH-150)/2, 18, 150, 24)]; self.titleLabel.textColor = RGB16(0x333333); self.titleLabel.font = [UIFont boldSystemFontOfSize:16]; self.titleLabel.text = @"我的认证"; self.titleLabel.textAlignment = NSTextAlignmentCenter; [self addSubview:self.titleLabel]; self.recordContentView = [[UIView alloc] init]; [self addSubview:self.recordContentView]; // 录制按钮 self.recordButton = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"singer_record_btn"]]; self.recordButton.userInteractionEnabled = YES; [self.recordContentView addSubview:self.recordButton]; // 进度圆环 self.progressLayer = [CAShapeLayer layer]; self.progressLayer.fillColor = [UIColor clearColor].CGColor; self.progressLayer.strokeColor = QXConfig.themeColor.CGColor; self.progressLayer.lineWidth = 4; self.progressLayer.strokeEnd = 0; // 确保圆环从顶部开始 self.progressLayer.strokeStart = 0; // 从起点开始 self.progressLayer.strokeEnd = 0; // 初始进度为0 // 如果需要动画效果,可以设置transform self.progressLayer.transform = CATransform3DIdentity; self.progressLayer.lineCap = kCALineCapRound; [self.recordContentView.layer addSublayer:self.progressLayer]; // 时间标签 self.timeLabel = [[UILabel alloc] init]; self.timeLabel.text = [self formattedTime:0]; self.timeLabel.textAlignment = NSTextAlignmentCenter; self.timeLabel.font = [UIFont systemFontOfSize:12]; self.timeLabel.textColor = RGB16A(0x000000, 0.45); [self addSubview:self.timeLabel]; // 状态标签 self.stateLabel = [[UILabel alloc] init]; self.stateLabel.text = @"长按录音"; self.stateLabel.textAlignment = NSTextAlignmentCenter; self.stateLabel.font = [UIFont systemFontOfSize:14]; self.stateLabel.textColor = [UIColor grayColor]; [self addSubview:self.stateLabel]; // // // 录制段数标签 // self.sessionLabel = [[UILabel alloc] init]; // self.sessionLabel.text = @""; // self.sessionLabel.textAlignment = NSTextAlignmentCenter; // self.sessionLabel.font = [UIFont systemFontOfSize:12]; // self.sessionLabel.textColor = [UIColor lightGrayColor]; // [self addSubview:self.sessionLabel]; // 试听按钮 self.playButton = [[UIButton alloc] init]; [self.playButton setTitle:@"试听" forState:UIControlStateNormal]; [self.playButton setTitleColor:RGB16A(0x000000, 0.45) forState:(UIControlStateNormal)]; self.playButton.titleLabel.font = [UIFont systemFontOfSize:12]; [self.playButton setImage:[UIImage imageNamed:@"singer_try_listen"] forState:(UIControlStateNormal)]; [self.playButton addTarget:self action:@selector(playButtonTapped) forControlEvents:UIControlEventTouchUpInside]; // self.playButton.hidden = YES; [self addSubview:self.playButton]; // 重置按钮 self.resetButton = [[UIButton alloc] init]; [self.resetButton setTitle:@"重置" forState:UIControlStateNormal]; [self.resetButton setImage:[UIImage imageNamed:@"singer_reset_record"] forState:(UIControlStateNormal)]; [self.resetButton setTitleColor:RGB16A(0x000000, 0.45) forState:(UIControlStateNormal)]; self.resetButton.titleLabel.font = [UIFont systemFontOfSize:12]; [self.resetButton addTarget:self action:@selector(resetButtonTapped) forControlEvents:UIControlEventTouchUpInside]; // self.resetButton.hidden = YES; [self addSubview:self.resetButton]; [self.recordContentView bringSubviewToFront:self.recordButton]; } - (void)setupAudioSession { // AVAudioSession *audioSession = [AVAudioSession sharedInstance]; // NSError *error = nil; // [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; // [audioSession setActive:YES error:&error]; // // if (error) { // NSLog(@"Audio session setup error: %@", error); // } AVAudioSession *audioSession = [AVAudioSession sharedInstance]; NSError *error = nil; // 使用 Playback 类别,会自动使用扬声器 [audioSession setCategory:AVAudioSessionCategoryPlayback error:&error]; // 或者使用 PlayAndRecord 但覆盖默认输出 [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:&error]; [audioSession setActive:YES error:&error]; if (error) { NSLog(@"Audio session setup error: %@", error); } } - (void)setupGestureRecognizers { // 长按手势 - 开始录制 UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; longPress.minimumPressDuration = 0.3; longPress.delegate = self; [self.recordButton addGestureRecognizer:longPress]; } -(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ return YES; } - (void)layoutSubviews { [super layoutSubviews]; CGFloat centerX = CGRectGetWidth(self.bounds) / 2; // 时间标签 self.timeLabel.frame = CGRectMake((SCREEN_WIDTH-150)/2, self.titleLabel.bottom+4, 150, 30); // 录制按钮 self.recordContentView.frame = CGRectMake(centerX - 50.5, self.timeLabel.bottom+22, 101, 101); self.recordButton.frame = CGRectMake(11, 11, 80, 80); // 进度圆环 CGRect progressRect = CGRectInset(self.recordContentView.bounds, 4, 4); UIBezierPath *progressPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(progressRect), CGRectGetMidY(progressRect)) radius:progressRect.size.width / 2 startAngle:-M_PI_2 // 从顶部开始(-90度) endAngle:-M_PI_2 + M_PI * 2 // 顺时针一圈 clockwise:YES]; self.progressLayer.path = progressPath.CGPath; // // 状态标签 self.stateLabel.frame = CGRectMake(0, self.recordContentView.bottom, CGRectGetWidth(self.bounds), 20); // // // 段数标签 // self.sessionLabel.frame = CGRectMake(0, 205, CGRectGetWidth(self.bounds), 20); // 按钮 self.playButton.frame = CGRectMake(SCREEN_WIDTH-38-35, self.recordContentView.top+35, 38, 60); self.resetButton.frame = CGRectMake(35, self.recordContentView.top+35, 38, 60); [self.playButton qx_layoutButtonNOSizeToFitWithEdgeInsetsStyle:(QXButtonEdgeInsetsStyleTop) imageTitleSpace:2]; [self.resetButton qx_layoutButtonNOSizeToFitWithEdgeInsetsStyle:(QXButtonEdgeInsetsStyleTop) imageTitleSpace:2]; } -(void)authAction{ ///去认证 ///去认证 if ((int)self.totalDuration < 5) { showToast(@"录音时长不得小于60秒"); return; } if (self.originalAudioFileURL==nil) { showToast(@"录制文件不存在"); return; } NSData *data = [NSData dataWithContentsOfURL:self.originalAudioFileURL]; @weakify(self) showLoadingInView(self.viewController.view); [[QXCOSUploadManager shareManager] audioUploadFile:data withObjectKey:self.originalAudioFileURL.lastPathComponent complete:^(NSString * _Nonnull fileUrl, QXCOSUploadImageState state) { @strongify(self); dispatch_async(dispatch_get_main_queue(), ^{ hideLoadingInView(self.viewController.view); if ([fileUrl isExist]) { [QXMineNetwork singerAuthWithSong:fileUrl successBlock:^(NSDictionary * _Nonnull dict) { showToast(@"上传成功"); } failBlock:^(NSError * _Nonnull error, NSString * _Nonnull msg) { showToast(@"上传失败"); }]; }else{ showToast(@"上传失败"); } }); }]; } #pragma mark - Gesture Handlers - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { CGPoint touchPoint = [gesture locationInView:self]; switch (gesture.state) { case UIGestureRecognizerStateBegan: [self stopPlayback]; NSLog(@"开始录制 - 第%ld段", (long)self.recordingSessionCount + 1); self.isCancelled = NO; [self startRecording]; break; case UIGestureRecognizerStateChanged: // 检查是否上滑取消 if (touchPoint.y < CGRectGetMinY(self.recordButton.frame) - 20) { if (!self.isCancelled) { NSLog(@"取消录制"); self.isCancelled = YES; [self updateUIForCancelled]; } } else { if (self.isCancelled) { NSLog(@"恢复录制"); self.isCancelled = NO; [self updateUIForRecording]; } } break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: NSLog(@"结束录制 - 当前段时长: %.1f秒", self.currentDuration); if (self.isCancelled) { [self cancelRecording]; } else { [self stopRecording]; } break; default: break; } } #pragma mark - Recording Control - (void)startRecording { if (self.state == AudioRecorderStateRecording) return; // 配置录音器 NSURL *audioFileURL = [self generateAudioFileURL]; NSDictionary *recordSettings = @{ AVFormatIDKey: @(kAudioFormatLinearPCM), AVSampleRateKey: @44100.0, AVNumberOfChannelsKey: @1, AVEncoderAudioQualityKey: @(AVAudioQualityHigh) }; NSError *error = nil; self.audioRecorder = [[AVAudioRecorder alloc] initWithURL:audioFileURL settings:recordSettings error:&error]; self.audioRecorder.delegate = self; self.audioRecorder.meteringEnabled = YES; if (error) { NSLog(@"录音器初始化失败: %@", error); if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didFailWithError:)]) { [self.delegate qxAudioRecorderView:self didFailWithError:error]; } return; } if ([self.audioRecorder record]) { self.state = AudioRecorderStateRecording; self.recordingSessionCount++; [self startRecordingTimer]; [self updateUIForRecording]; if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didChangeState:)]) { [self.delegate qxAudioRecorderView:self didChangeState:self.state]; } } } - (void)stopRecording { if (self.state != AudioRecorderStateRecording) return; [self.recordingTimer invalidate]; self.recordingTimer = nil; NSTimeInterval segmentDuration = self.currentDuration; [self.audioRecorder stop]; // 保存录制的音频段 if (segmentDuration > 0.5) { // 只保存超过0.5秒的有效录音 [self.recordedSegments addObject:self.audioRecorder.url]; self.totalDuration += segmentDuration; NSLog(@"保存音频段,时长: %.1f秒,总时长: %.1f秒", segmentDuration, self.totalDuration); } self.audioRecorder = nil; self.currentDuration = 0; self.state = AudioRecorderStateStopped; [self updateUIForStopped]; // 如果有录音段,合并音频 if (self.recordedSegments.count > 0) { [self mergeAudioSegments]; } if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didChangeState:)]) { [self.delegate qxAudioRecorderView:self didChangeState:self.state]; } } - (void)cancelRecording { if (self.state != AudioRecorderStateRecording) return; [self.recordingTimer invalidate]; self.recordingTimer = nil; [self.audioRecorder stop]; [self.audioRecorder deleteRecording]; self.audioRecorder = nil; self.currentDuration = 0; self.state = AudioRecorderStateStopped; [self updateUIForStopped]; if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didChangeState:)]) { [self.delegate qxAudioRecorderView:self didChangeState:self.state]; } } #pragma mark - Audio Merging - (void)mergeAudioSegments { if (self.recordedSegments.count == 0) return; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 创建合并文件 NSURL *mergedFileURL = [self generateAudioFileURLWithExtension:@"m4a"]; // 创建音频组合 AVMutableComposition *composition = [AVMutableComposition composition]; CMTime currentTime = kCMTimeZero; for (NSURL *audioURL in self.recordedSegments) { AVURLAsset *audioAsset = [AVURLAsset assetWithURL:audioURL]; CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, audioAsset.duration); // 添加音频轨道 AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; AVAssetTrack *sourceAudioTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject]; if (sourceAudioTrack) { [audioTrack insertTimeRange:timeRange ofTrack:sourceAudioTrack atTime:currentTime error:nil]; currentTime = CMTimeAdd(currentTime, audioAsset.duration); } } // 导出合并的音频 AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:composition presetName:AVAssetExportPresetAppleM4A]; exportSession.outputURL = mergedFileURL; exportSession.outputFileType = AVFileTypeAppleM4A; [exportSession exportAsynchronouslyWithCompletionHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ if (exportSession.status == AVAssetExportSessionStatusCompleted) { self.originalAudioFileURL = mergedFileURL; NSLog(@"音频合并完成,总时长: %.1f秒", self.totalDuration); // 转换为MP3 [self convertToMP3]; } else { NSLog(@"音频合并失败: %@", exportSession.error); } }); }]; }); } #pragma mark - Reset Function - (void)resetRecording { NSLog(@"重置录音"); [self stopPlayback]; [self cancelRecording]; // 清理所有录音段 for (NSURL *segmentURL in self.recordedSegments) { [[NSFileManager defaultManager] removeItemAtURL:segmentURL error:nil]; } [self.recordedSegments removeAllObjects]; // 清理合并文件 if (self.originalAudioFileURL) { [[NSFileManager defaultManager] removeItemAtURL:self.originalAudioFileURL error:nil]; self.originalAudioFileURL = nil; } if (self.mp3AudioFileURL) { [[NSFileManager defaultManager] removeItemAtURL:self.mp3AudioFileURL error:nil]; self.mp3AudioFileURL = nil; } // 重置状态 self.totalDuration = 0; self.currentDuration = 0; self.recordingSessionCount = 0; self.state = AudioRecorderStateReady; [self updateUIForReady]; if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didChangeState:)]) { [self.delegate qxAudioRecorderView:self didChangeState:self.state]; } } #pragma mark - Playback Control - (void)playButtonTapped { if (self.state == AudioRecorderStatePlaying) { [self stopPlayback]; } else { [self playRecording]; } } - (void)playRecording { if (self.state == AudioRecorderStatePlaying) return; NSURL *playbackURL = self.mp3AudioFileURL ?: self.originalAudioFileURL; if (!playbackURL){ showToast(@"您还未录制声音"); return; } // 播放前切换到扬声器 // AVAudioSession *audioSession = [AVAudioSession sharedInstance]; // NSError *error = nil; // [audioSession setCategory:AVAudioSessionCategoryPlayback error:&error]; // [audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]; // [audioSession setActive:YES error:&error]; NSError *playerError = nil; self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:playbackURL error:&playerError]; self.audioPlayer.delegate = self; if (playerError) { NSLog(@"播放器初始化失败: %@", playerError); return; } if ([self.audioPlayer play]) { self.state = AudioRecorderStatePlaying; [self updateUIForPlaying]; if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didChangeState:)]) { [self.delegate qxAudioRecorderView:self didChangeState:self.state]; } } } - (void)stopPlayback { if (self.state != AudioRecorderStatePlaying) return; [self.audioPlayer stop]; self.audioPlayer = nil; self.state = AudioRecorderStateStopped; [self updateUIForStopped]; if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didChangeState:)]) { [self.delegate qxAudioRecorderView:self didChangeState:self.state]; } } - (void)resetButtonTapped { [self resetRecording]; } #pragma mark - Timer - (void)startRecordingTimer { self.currentDuration = 0; self.recordingTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(updateRecordingProgress) userInfo:nil repeats:YES]; } - (void)updateRecordingProgress { if (self.state != AudioRecorderStateRecording) return; self.currentDuration += 0.1; // 更新UI [self updateProgress]; [self updateTimeLabel]; // 通知代理进度更新 if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didUpdateProgress:totalDuration:)]) { [self.delegate qxAudioRecorderView:self didUpdateProgress:self.currentDuration totalDuration:self.totalDuration + self.currentDuration]; } // 检查是否达到最大时长 if ((self.totalDuration + self.currentDuration) >= self.maxDuration) { [self stopRecording]; } } #pragma mark - UI Updates - (void)updateUIForReady { self.progressLayer.strokeEnd = 0; self.timeLabel.text = [self formattedTime:0]; self.stateLabel.text = @"长按录音"; // // self.playButton.hidden = YES; // self.resetButton.hidden = YES; } - (void)updateUIForRecording { self.stateLabel.text = self.isCancelled ? @"松开手指取消" : @"录音中...松开结束"; // self.playButton.hidden = YES; // self.resetButton.hidden = YES; } - (void)updateUIForStopped { self.stateLabel.text = @"继续录音"; if (self.totalDuration > 0) { if (self.showPlaybackButton) { // self.playButton.hidden = NO; // self.resetButton.hidden = NO; [self.playButton setTitle:@"试听" forState:UIControlStateNormal]; [self.playButton setImage:[UIImage imageNamed:@"singer_try_listen"] forState:UIControlStateNormal]; } } else { // self.playButton.hidden = YES; // self.resetButton.hidden = YES; } // self.stateLabel.textColor = [UIColor grayColor]; } - (void)updateUIForPlaying { // self.stateLabel.text = @"试听中..."; // self.stateLabel.textColor = [UIColor blueColor]; [self.playButton setTitle:@"停止" forState:UIControlStateNormal]; [self.playButton setImage:[UIImage imageNamed:@"singer_listen_stop"] forState:UIControlStateNormal]; } - (void)updateUIForCancelled { // self.stateLabel.text = @"松开手指取消"; // self.stateLabel.textColor = [UIColor redColor]; } - (void)updateProgress { CGFloat totalProgress = (self.totalDuration + self.currentDuration) / self.maxDuration; self.progressLayer.strokeEnd = totalProgress; // // 根据进度改变颜色 // if (totalProgress > 0.8) { // self.progressLayer.strokeColor = [UIColor redColor].CGColor; // } else if (totalProgress > 0.6) { // self.progressLayer.strokeColor = [UIColor orangeColor].CGColor; // } else { // self.progressLayer.strokeColor = [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0].CGColor; // } } - (void)updateTimeLabel { NSTimeInterval currentTotal = self.totalDuration + self.currentDuration; self.timeLabel.text = [self formattedTime:currentTotal]; } #pragma mark - MP3 Conversion - (void)convertToMP3 { // if (!self.originalAudioFileURL) return; // // dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // NSString *mp3FileName = [NSString stringWithFormat:@"%@.mp3", [[NSUUID UUID] UUIDString]]; // NSString *mp3FilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:mp3FileName]; // self.mp3AudioFileURL = [NSURL fileURLWithPath:mp3FilePath]; // // @try { // int read, write; // // FILE *pcm = fopen([[self.originalAudioFileURL path] UTF8String], "rb"); // FILE *mp3 = fopen([mp3FilePath UTF8String], "wb"); // // const int PCM_SIZE = 8192; // const int MP3_SIZE = 8192; // short int pcm_buffer[PCM_SIZE * 2]; // unsigned char mp3_buffer[MP3_SIZE]; // // lame_t lame = lame_init(); // lame_set_in_samplerate(lame, 44100); // lame_set_VBR(lame, vbr_default); // lame_init_params(lame); // // do { // read = (int)fread(pcm_buffer, 2 * sizeof(short int), PCM_SIZE, pcm); // if (read == 0) // write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE); // else // write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE); // // fwrite(mp3_buffer, write, 1, mp3); // // } while (read != 0); // // lame_close(lame); // fclose(mp3); // fclose(pcm); // } // @catch (NSException *exception) { // NSLog(@"MP3转换失败: %@", exception); // self.mp3AudioFileURL = nil; // } // @finally { // dispatch_async(dispatch_get_main_queue(), ^{ // if (self.mp3AudioFileURL && [self.delegate respondsToSelector:@selector(audioRecorderView:didFinishRecordingWithMP3File:duration:)]) { // [self.delegate audioRecorderView:self didFinishRecordingWithMP3File:self.mp3AudioFileURL duration:self.totalDuration]; // } // }); // } // }); } #pragma mark - Utility Methods - (NSURL *)generateAudioFileURL { return [self generateAudioFileURLWithExtension:@"caf"]; } - (NSURL *)generateAudioFileURLWithExtension:(NSString *)extension { long long time = [[NSDate date] timeIntervalSince1970]; NSString *fileName = [NSString stringWithFormat:@"%lld.%@", time ,extension]; NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; return [NSURL fileURLWithPath:filePath]; } - (NSString *)formattedTime:(NSTimeInterval)time { int minutes = (int)time / 60; int seconds = (int)time % 60; int maxMinutes = (int)_maxDuration / 60; int maxSeconds = (int)_maxDuration % 60; return [NSString stringWithFormat:@"%02d:%02d | %02d:%02d", minutes, seconds,maxMinutes,maxSeconds]; } - (NSString *)getRecordingStatus { if (self.totalDuration > 0) { return [NSString stringWithFormat:@"已录制%.1f秒,共%ld段", self.totalDuration, (long)self.recordingSessionCount]; } else { return @"未开始录制"; } } #pragma mark - AVAudioRecorderDelegate - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag { NSLog(@"录音完成: %@", flag ? @"成功" : @"失败"); } - (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error { NSLog(@"录音编码错误: %@", error); if ([self.delegate respondsToSelector:@selector(qxAudioRecorderView:didFailWithError:)]) { [self.delegate qxAudioRecorderView:self didFailWithError:error]; } } #pragma mark - AVAudioPlayerDelegate - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag { [self stopPlayback]; } - (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error { NSLog(@"播放解码错误: %@", error); [self stopPlayback]; } #pragma mark - Public Properties - (NSURL *)currentAudioFileURL { return self.mp3AudioFileURL ?: self.originalAudioFileURL; } - (void)dealloc { [self.recordingTimer invalidate]; [self.audioRecorder stop]; [self.audioPlayer stop]; } @end