[iOS]音频录制和播放

GitHub:https://github.com/Gamin-fzym/VoicePlayerDemo
Demo:https://download.csdn.net/download/u012881779/11351934
遇到了一个录制个人语音介绍的需求,需要将录制的caf转mp3上传服务器。因为安卓端说将amr转MP3比较麻烦,所以demo这里对amr文件的播放进行了支持。(最后小程序中也不能播放amr,安卓端还是成功将amr转码为mp3了,麻烦这种事情就怕认真)
[VoiceCell XIB]

#import <UIKit/UIKit.h>
#import "GSSmartCardCustomModel.h"

@interface GSSmartCardVoiceCell : UITableViewCell

@property (weak, nonatomic) IBOutlet UIImageView *photoIV;      // 头像
@property (weak, nonatomic) IBOutlet UIView *longPressView;     // 长按视图
@property (weak, nonatomic) IBOutlet UIButton *longPressButton; // 长按录制按钮
@property (weak, nonatomic) IBOutlet UIView *voiceView;         // 语音视图
@property (weak, nonatomic) IBOutlet UIImageView *voiceMarkIV;  // 播放动画
@property (weak, nonatomic) IBOutlet UILabel *secondLab;        // 语音秒数
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *cancelWConstraint; // 删除宽度
@property (assign, nonatomic) NSInteger enterMark;   // 0:他的名片 1:我的名片
@property (weak, nonatomic) GSSmartCardCustomModel *dataModel;
typedef void (^TapCancelVoiceBlock)(NSInteger mark, GSSmartCardCustomModel *dModel); // 1:点击删除语音按钮 2:录制语音上传成功
@property (strong, nonatomic) TapCancelVoiceBlock cancelBlock;

- (void)initWithObject:(id)object IndexPath:(NSIndexPath *)indexPath;

@end
#import "GSSmartCardVoiceCell.h"
#import "TLRecorderIndicatorView.h"
#import "CDPAudioRecorder.h"
#import "View+MASAdditions.h"
#import "GSVoicePlayer.h"
#import "MBProgressHUD.h"
#import "UIView+Toast.h"

#define KWindow [UIApplication sharedApplication].keyWindow

@interface GSSmartCardVoiceCell () <CDPAudioRecorderDelegate> {
    CDPAudioRecorder *_recorder; // recorder对象
    UIImageView *_imageView; // 音量图片
    UIButton *_recordBt; // 录音bt
    UIButton *_playBt;   // 播放bt
    UIButton *_deleBt;   // 删除bt
    double currentTime;
}
// 录音展示view
@property (nonatomic, strong) TLRecorderIndicatorView *recorderIndicatorView;;
@property (nonatomic, assign) BOOL isClick;

@end

@implementation GSSmartCardVoiceCell

- (void)awakeFromNib {
    [super awakeFromNib];
    [_longPressButton addTarget:self action:@selector(startRecord:) forControlEvents:UIControlEventTouchDown];
    [_longPressButton addTarget:self action:@selector(endRecord:) forControlEvents:UIControlEventTouchUpInside];
    [_longPressButton addTarget:self action:@selector(cancelRecord:) forControlEvents:UIControlEventTouchDragExit];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(GSPlayFinishedNotification:) name:@"GSPlayFinishedNotification" object:nil];
}

// 判断字符串是否不全为空
- (BOOL)judgeStringIsNull:(NSString *)string {
    if (![[string class] isSubclassOfClass:[NSString class]]) {
        return NO;
    }
    if ([[string class] isSubclassOfClass:[NSNumber class]]) {
        return YES;
    }
    BOOL result = NO;
    if (string != nil && string.length > 0) {
        for (int i = 0; i < string.length; i ++) {
            NSString *subStr = [string substringWithRange:NSMakeRange(i, 1)];
            if (![subStr isEqualToString:@" "] && ![subStr isEqualToString:@""]) {
                result = YES;
            }
        }
    }
    return result;
}

- (UIViewController *)viewController {
    for (UIView* next = [self superview]; next; next = next.superview) {
        UIResponder* nextResponder = [next nextResponder];
        if ([nextResponder isKindOfClass:[UIViewController class]]) {
            return (UIViewController*)nextResponder;
        }
    }
    return nil;
}

- (void)releaseAction {
    // 结束播放
    [_recorder stopPlaying];
    // 结束录音
    [_recorder stopRecording];
    _cancelWConstraint.constant = 0;
    _secondLab.text = @"";
    _longPressView.hidden = YES;
    _voiceView.hidden = YES;
    _voiceMarkIV.image = [UIImage imageNamed:@"erty2"];
}

- (void)initWithObject:(id)object IndexPath:(NSIndexPath *)indexPath {
    [self releaseAction];
    if (object) {
        _dataModel = (GSSmartCardCustomModel *)object;
        _photoIV.image = [UIImage imageNamed:@"mrtx-yx"];
    
        [self updateViewAction];
    }
}

// 结束播放通知
- (void)GSPlayFinishedNotification:(id)sender {
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        // 通知主线程刷新
        [weakSelf.voiceMarkIV stopAnimating];
        weakSelf.voiceMarkIV.image = [UIImage imageNamed:@"erty2"];
    });
}

// 播放动画
- (void)imageViewAnimation {
    NSArray *imgArr = [[NSArray alloc] initWithObjects:[UIImage imageNamed:@"erty0"],[UIImage imageNamed:@"erty1"],[UIImage imageNamed:@"erty2"], nil];
    _voiceMarkIV.animationImages = imgArr;
    // 动画总时间
    _voiceMarkIV.animationDuration = imgArr.count*0.5;
    // 动画重复次数
    _voiceMarkIV.animationRepeatCount = 1000;
    [_voiceMarkIV startAnimating];
}

// 更新视图
- (void)updateViewAction {
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 子线程中处理耗时操作
        if (weakSelf.dataModel.dFileModel && [self judgeStringIsNull:weakSelf.dataModel.dFileModel.filePath]) {
            NSInteger duration = [weakSelf.dataModel.dFileModel.fileLength integerValue];
            if (!(duration > 0)) {
                duration = [GSVoicePlayer audioDurationFromURL:weakSelf.dataModel.dFileModel.filePath];
                weakSelf.dataModel.dFileModel.fileLength = [NSString stringWithFormat:@"%ld",(long)duration];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                // 通知主线程刷新
                if (duration > 0) {
                    weakSelf.secondLab.text = [NSString stringWithFormat:@"%ld″",(long)duration];
                }
            });
        }
    });
    
    dispatch_async(dispatch_get_main_queue(), ^{
        weakSelf.secondLab.text = @"";
        // 是否有返回音频
        BOOL haveVoice = NO;
        if (weakSelf.dataModel.dFileModel && [self judgeStringIsNull:weakSelf.dataModel.dFileModel.filePath]) {
            haveVoice = YES;
        }
        weakSelf.cancelWConstraint.constant = 0;
        if (haveVoice) {
            weakSelf.longPressView.hidden = YES;
            weakSelf.voiceView.hidden = NO;
            if (weakSelf.enterMark == 1) {
                weakSelf.cancelWConstraint.constant = 44;
            }
        } else {
            weakSelf.longPressView.hidden = NO;
            weakSelf.voiceView.hidden = YES;
        }
    });
}

// 点击语音播放
- (IBAction)tapVoiceAction:(id)sender {
    NSString *voicePath = _dataModel.dFileModel.filePath;
    if (_dataModel.dFileModel && [self judgeStringIsNull:voicePath]) {
        [self imageViewAnimation];
        GSVoicePlayer *player = [GSVoicePlayer sharedVoicePlayerMethod];
        [player playWithFilePath:voicePath];
    }
}

// 点击删除按钮
- (IBAction)tapCancelButtonAction:(id)sender {
    __weak typeof(self) weakSelf = self;
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:@"是否删除录音" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDestructive handler:nil];
    UIAlertAction *sureAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        if (weakSelf.dataModel.dFileModel && [self judgeStringIsNull:weakSelf.dataModel.dFileModel.ID]) {
            [self deleteFileWithId:weakSelf.dataModel.dFileModel.ID];
        }
    }];
    [alert addAction:cancelAction];
    [alert addAction:sureAction];
    [[self viewController] presentViewController:alert animated:YES completion:nil];
}

// 删除录音
- (void)deleteFileWithId:(NSString *)photoId {
    if (self.cancelBlock) {
        self.cancelBlock(1, self.dataModel);
    }
}

// alertView提示
- (void)alertWithMessage:(NSString *)message {
    [[self viewController].view makeToast:message];
    // [KWindow makeToast:@"提交失败"];
}

#pragma mark - CDPAudioRecorderDelegate代理事件
// 更新音量分贝数峰值(0~1)
- (void)updateVolumeMeters:(CGFloat)value {
    NSInteger no = 0;
    if (value>0 && value<=0.14) {
        no = 1;
    } else if (value <= 0.28) {
        no = 2;
    } else if (value <= 0.42) {
        no = 3;
    } else if (value <= 0.56) {
        no = 4;
    } else if (value <= 0.7) {
        no = 5;
    } else if (value <= 0.84) {
        no = 6;
    } else{
        no = 7;
    }
    NSString *imageName = [NSString stringWithFormat:@"mic_%ld",(long)no];
    _imageView.image = [UIImage imageNamed:imageName];
}

// 录音结束(url为录音文件地址,isSuccess是否录音成功)
- (void)recordFinishWithUrl:(NSString *)url isSuccess:(BOOL)isSuccess {
    [self.recorderIndicatorView removeFromSuperview];
    // url为得到的caf录音文件地址,可直接进行播放,也可进行转码为amr、mp3上传服务器
    NSLog(@"录音完成,文件地址:%@",url);
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [path stringByAppendingPathComponent:@"CDPAudioFiles/CDPAudioRecord.caf"];
    NSData *voiceData = [NSData dataWithContentsOfFile:filePath];
    if (voiceData) {
        // 异步caf转MP3
        [MBProgressHUD showHUDAddedTo:KWindow animated:YES];
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [GSVoicePlayer transformCAFToMP3:[NSURL URLWithString:filePath] Success:^(NSInteger mark) {
                if (mark) {
                    [self dealFileAction];
                } else {
                    [self alertWithMessage:@"转码失败"];
                }
            }];
        });
    } else {
        [self alertWithMessage:@"录音失败"];
    }
}

// 转换格式成功后 上传+提交
- (void)dealFileAction {
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [path stringByAppendingPathComponent:@"CDPAudioFiles/CDPAudioRecord.mp3"];
    NSData *voiceData = [NSData dataWithContentsOfFile:filePath];
    if (voiceData) {
        NSLog(@"");
        GSSmartFileModel *fileModel2 = [GSSmartFileModel new];
        fileModel2.filePath = filePath;
        fileModel2.fileLength = [NSString stringWithFormat:@"%.f",currentTime];
        fileModel2.ID = @"234";
        self.dataModel.dFileModel = fileModel2;
        if (self.cancelBlock) {
            self.cancelBlock(2, self.dataModel);
        }
        [self hideHUDWithAlert:@"提交成功"];
    } else {
        [self hideHUDWithAlert:@"转码失败"];
    }
}

- (void)hideHUDWithAlert:(NSString *)alert {
    dispatch_async(dispatch_get_main_queue(), ^{
        [MBProgressHUD hideHUDForView:KWindow animated:YES];
        if (![alert isEqualToString:@""]) {
            [self alertWithMessage:alert];
        }
    });
}

#pragma mark - 各录音点击事件
// 按下开始录音
- (void)startRecord:(UIButton *)recordBtn {
    _isClick =NO;
    [CDPAudioRecorder getAudioRecordFilePathWithMark:NO];
    // 初始化录音recorder
    _recorder = [CDPAudioRecorder shareRecorder];
    _recorder.delegate = self;
    [_recorder startRecording];
    [self.recorderIndicatorView setStatus:TLRecorderStatusRecording];
    [[self viewController].view addSubview:self.recorderIndicatorView];
    [self.recorderIndicatorView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.mas_equalTo([self viewController].view);
        make.size.mas_equalTo(CGSizeMake(150, 150));
    }];
    
    // 音量图片
    _imageView = [[UIImageView alloc] init];
    _imageView.image = [UIImage imageNamed:@"mic_0"];
    [self.recorderIndicatorView addSubview:_imageView];
    [_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerY.mas_equalTo(0);
        make.centerX.mas_equalTo(0);
        make.width.mas_equalTo(64);
        make.height.mas_equalTo(64);
    }];
}

// 点击松开结束录音
- (void)endRecord:(UIButton *)recordBtn {
    [self.recorderIndicatorView removeFromSuperview];
    currentTime = _recorder.recorder.currentTime;
    NSLog(@"本次录音时长%lf",currentTime);
    if (currentTime < 1) {
        // 时间太短
        _imageView.image = [UIImage imageNamed:@"mic_0"];
        if (!_isClick) {
            [self alertWithMessage:@"说话时间太短"];
        }
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self->_recorder stopRecording];
            [self->_recorder deleteAudioFile];
        });
    } else {
        // 成功录音
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self->_recorder stopRecording];
            dispatch_async(dispatch_get_main_queue(), ^{
                self->_imageView.image = [UIImage imageNamed:@"mic_0"];
            });
        });
        NSLog(@"已成功录音");
    }
    _isClick = NO;
}

// 手指从按钮上移除,取消录音
- (void)cancelRecord:(UIButton *)recordBtn {
    _isClick =NO;
    [self.recorderIndicatorView removeFromSuperview];
    _imageView.image = [UIImage imageNamed:@"mic_0"];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self->_recorder stopRecording];
        [self->_recorder deleteAudioFile];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self alertWithMessage:@"已取消录音"];
        });
    });
}

#pragma mark - 播放点击事件

// 播放录音
- (void)play {
    // 播放内部默认地址刚才生成的本地录音文件,不需要转码
    // [_recorder playAudioFile];
}

- (void)dele {
    [_recorder deleteAudioFile];
}

- (TLRecorderIndicatorView *)recorderIndicatorView {
    if (_recorderIndicatorView == nil) {
        _recorderIndicatorView = [[TLRecorderIndicatorView alloc] init];
    }
    return _recorderIndicatorView;
}

@end

[VoicePlayer 播放器]

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface GSVoicePlayer : NSObject <AVAudioPlayerDelegate>

+ (id)sharedVoicePlayerMethod;

/// 播放音频
- (void)playWithFilePath:(NSString *)filePath;

/// 停止
- (void)stopVoice;

/// 获取音频时长
+ (NSTimeInterval)audioDurationFromURL:(NSString *)url;

// caf转MP3 0:转换失败 1:转换成功
typedef void (^TransformCafToMP3Block)(NSInteger mark);
+ (NSURL *)transformCAFToMP3:(NSURL *)sourceUrl Success:(TransformCafToMP3Block)success;

@end
#import "GSVoicePlayer.h"
// 导入系统框架
#import "lame.h"
#import "gsAmrFileCodec.h"

static GSVoicePlayer *voicePlayerObject = nil;
static AVPlayer *avPlayer = nil;
static AVAudioPlayer *audioPlayer = nil;

@implementation GSVoicePlayer

+ (id)sharedVoicePlayerMethod {
    @synchronized (self){
        if (!voicePlayerObject) {
            voicePlayerObject = [[GSVoicePlayer alloc] init];
        }
        return voicePlayerObject;
    }
    return voicePlayerObject;
}

- (void)playWithFilePath:(NSString *)filePath {
    if (![filePath isKindOfClass:[NSString class]]) {
        filePath = @"";
    }
    if ([filePath containsString:@".amr"] || [filePath containsString:@"/var/mobile/"]) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self playAVAudioPlayerWithFilePath:filePath];
        });
    } else {
        [self playAVPlayerWithFilePath:filePath];
    }
}

- (void)stopVoice {
    [self stopVoiceAction];
    [self finishedNFAction];
}

- (void)stopVoiceAction {
    avPlayer = nil;
    audioPlayer = nil;
}

// 结束播放通知
- (void)finishedNFAction {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"GSPlayFinishedNotification" object:nil];
}

#pragma mark - AVPlayer
// 播放网络音频
- (void)playAVPlayerWithFilePath:(NSString *)filePath {
    if (avPlayer.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
        [self stopVoice];
    } else {
        [self stopVoiceAction];
        avPlayer = [[AVPlayer alloc] init];
        // 设置播放的项目
        NSURL *url = [NSURL URLWithString:filePath];
        AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:url];
        [avPlayer replaceCurrentItemWithPlayerItem:item];
        [avPlayer play];
        // 添加播放结束监听
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:avPlayer.currentItem];
    }
}

// 播放完成
- (void)playbackFinished:(id)sender {
    [self finishedNFAction];
}

#pragma mark - AVAudioPlayer
// 播放本地音频
- (void)playAVAudioPlayerWithFilePath:(NSString *)filePath {
    if (audioPlayer.isPlaying) {
        [self stopVoice];
    } else {
        [self stopVoiceAction];
        NSError *playError;
        // audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL URLWithString:filePath] error:&playError];
        NSData *wavData;
        if ([filePath containsString:@"/var/mobile/"]) {
            // 播放本地音频
            wavData = [NSData dataWithContentsOfFile:filePath];
        } else {
            // 用来播放安卓的amr格式 方法必须放在异步中执行下载和转码
            NSData *amrData = [NSData dataWithContentsOfURL:[NSURL URLWithString:filePath]];
            wavData = DecodeAMRToWAVE(amrData);
        }
        audioPlayer = [[AVAudioPlayer alloc] initWithData:wavData error:&playError];
        audioPlayer.delegate = self;
        [audioPlayer prepareToPlay];
        [audioPlayer play];
    }
}

#pragma mark - AVAudioPlayerDelegate

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    [self finishedNFAction];
}

- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error {
    [self finishedNFAction];
}

#pragma mark - 其它功能
// 获取音频时长
+ (NSTimeInterval)audioDurationFromURL:(NSString *)url {
    AVURLAsset *audioAsset = nil;
    NSDictionary *dic = @{AVURLAssetPreferPreciseDurationAndTimingKey:@(YES)};
    if ([url hasPrefix:@"http"]) {
        audioAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:url] options:dic];
    } else {
        audioAsset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:url] options:dic];
    }
    CMTime audioDuration = audioAsset.duration;
    float audioDurationSeconds = CMTimeGetSeconds(audioDuration);
    return audioDurationSeconds;
}

// caf转MP3
+ (NSURL *)transformCAFToMP3:(NSURL *)sourceUrl Success:(TransformCafToMP3Block)success {
    NSURL *mp3FilePath,*audioFileSavePath;
    
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    mp3FilePath = [NSURL URLWithString:[path stringByAppendingPathComponent:@"CDPAudioFiles/CDPAudioRecord.mp3"]];
    
    @try {
        int read, write;
        
        FILE *pcm = fopen([[sourceUrl absoluteString] cStringUsingEncoding:1], "rb");   //source 被转换的音频文件位置
        fseek(pcm, 4*1024, SEEK_CUR);                                                   //skip file header
        FILE *mp3 = fopen([[mp3FilePath absoluteString] cStringUsingEncoding:1], "wb"); //output 输出生成的Mp3文件位置
        
        NSLog(@"sour-- %@   last-- %@",sourceUrl,mp3FilePath);
        
        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, 8000.0); // 这里与录制音频时的“采样率”必须一致
        lame_set_VBR(lame, vbr_default);
        lame_init_params(lame);
        
        do {
            read = 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(@"%@",[exception description]);
        if (success) {
            success(0);
        }
    }
    @finally {
        audioFileSavePath = mp3FilePath;
        NSLog(@"MP3生成成功: %@",audioFileSavePath);
        if (success) {
            success(1);
        }
    }
    return audioFileSavePath;
}

@end

示意图:

Demo目录:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值