iOS语音录制、转码及播放

前言

由于业务需要,在开发过程中需要使用到语音方面的知识,并且在和Android同步开发时,需要用到转码。因此,语音的录制、播放及转码在APP中需要实现。

准备工作

1. 界面设计

由于本例是语音demo,因此不要求界面多么美观,功能齐全即可,使用autolayout构建的界面如下图。
界面设计图

(1)NavVC作为Initial View,由导航控制整个页面跳转;
(2)根视图中包括了录制和播放功能的实现;
(3)Audios页面中对所录制的音频文件进行查看和删除;
(4)PlayAudio页面是对音频文件进行播放,播放时带有动画效果。

2. 类库

Demo中使用的类库有AmrVoiceConverter和UIView+FrameEx类库,前者是wav和amr互转的类库,后者是UIView的分类类库,旨在更好操作UIView的frame。另外,使用AVFoundation类库进行音频的录制和播放。

说明:wav是iOS上录制生成的文件格式,其体积较大,录制10秒生成的文件在100k左右,而amr格式是Android平台上使用的音频格式,其体积较小。因此为保证和Android平台的一致,在iOS平台上需要将wav转化为amr文件,这也同样节省了流量消耗。

详细实现

在这里不针对某一个页面进行具体实现,对其中的重要功能进行说明。如需要查看所有的实现,请移步文章底部。

1. 录制功能的实现

录制功能使用AVFoundation类库中的API,

(1)APP请求许可

首先需要请求APP的许可,使用语音设备。前提是在Info.plist中配置了键值对。
Privacy - Microphone Usage Description
该描述要详细,否则会因模糊的语句导致被拒。

请求APP的许可,调用后会在APP中弹出alert。
1.png
其实现代码如下
- (void)requestRecordingPermission: (void(^) (BOOL))callback {
    AVAudioSession* audioSession = [AVAudioSession sharedInstance];
    
    if ([audioSession respondsToSelector: @selector(requestRecordPermission:)]) {
        [audioSession performSelector: @selector(requestRecordPermission:)
                           withObject: ^(BOOL granted) {
                               callback(granted);
                           }];
    }
}

(2)录音

主要分为设置AVAudioSession、设置文件路径和设置AVAudioRecorder。
a. AVAudioSession的设置

    AVAudioSession* audioSession = [AVAudioSession sharedInstance];
    NSError* error;
    
    [audioSession setCategory: AVAudioSessionCategoryPlayAndRecord
                        error: &error];
    
    if (audioSession == nil) {
        // 弹出Alert
        return ;
    }
    
    [audioSession setActive: YES
                      error: nil];

b. 文件路径的设置
语音文件需要单独创建一个文件夹用于存储,将其存入DOCUMENT_PATH下面的audios文件夹中亦可。

#define DOCUMENT_PATH                   [NSSearchPathForDirectoriesInDomains(   \
                                            NSDocumentDirectory, NSUserDomainMask, \
                                            YES) objectAtIndex: 0]
#define AUDIO_FOLDER_NAME               @"audios"
#define AUDIO_FOLDER_PATH               [DOCUMENT_PATH stringByAppendingPathComponent:  \
                                            AUDIO_FOLDER_NAME]

c.设置AVAudioRecorder
AVAudioRecorder中录音类,需要设置录音的settings,如采样频率、音频格式、采样位数、音频通道和录音质量。

NSDictionary* recordSettings = @{
             AVSampleRateKey: @8000.0f,                         // 采样率
             AVFormatIDKey: @(kAudioFormatLinearPCM),           // 音频格式
             AVLinearPCMBitDepthKey: @16,                       // 采样位数
             AVNumberOfChannelsKey: @1,                         // 音频通道
             AVEncoderAudioQualityKey: @(AVAudioQualityHigh)    // 录音质量
             };
    
    _avAudioRecorder = [[AVAudioRecorder alloc] initWithURL: _curWavFileUrl
                                                   settings: recordSettings
                                                      error: nil];
    
    if (!_avAudioRecorder) {
        // 弹出Alert
        return ;
    }
    
    _avAudioRecorder.meteringEnabled = YES;
    [_avAudioRecorder prepareToRecord];
    [_avAudioRecorder record];

2. 播放功能的实现

播放功能相对较简单,只需要设置AVAudioPlayer即可。

- (IBAction)playBBIAction:(UIBarButtonItem *)sender {
    if (_avAudioPlayer && _avAudioPlayer.isPlaying) {
        [_avAudioPlayer stop];
        
        return ;
    }
    
    if ([_avAudioRecorder isRecording])
        return ;
    
    if ([[FileManager manager] isFileExistsAtPath: _curWavFilePath]) {
        _avAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:
                          [NSURL fileURLWithPath: _curWavFilePath]
                                                                error: nil];
        [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback
                                               error: nil];
        [_avAudioPlayer play];
    }
}

3. 转码功能的实现

添加类库时,无论以cocoapods添加或者直接拖拽至工程,若使用SVN或git文件,需要保证其ignore文件中对.a文件的忽略,否则在上传或者下载时忽略掉.a文件,造成编译时报丢失链接库的错误。
转码使用VoiceConverter类库,其提供了API如下

@interface VoiceConverter : NSObject

+ (BOOL)amrToWav:(NSString*)_amrPath wavSavePath:(NSString*)_savePath;
+ (BOOL)wavToAmr:(NSString*)_wavPath amrSavePath:(NSString*)_savePath;
+ (NSData *)convertToRawAmrDataWithData:(NSData *)data;
+ (NSData *)amrToWavWithAmrData:(NSData *)amrData;
+ (NSData *)wavToAmrWithWavData:(NSData *)wavData;

@end

在实际的业务需要中,使用到的是wav->amr,再由amr的NSData转为wav的NSData,即使用API中的第二个和第四个方法。

4. 界面效果的实现

(1)录制时钟表滚动效果

在录制时,刷新时间的同时,圆盘上有红色线按时钟表顺时针滚动,如图

圆盘滚动效果
使用NSTimer来控制:时间label使用NSTimer来进行刷新,每隔1s刷新一次;滚动效果的时间间隔若设置为1s,1s画的区域为1/60,这样得出的效果不是连续的。由于人的视觉暂留时间是0.1s,因此设置为0.1s或者小于0.1s间隔,超出人眼辨别范围,便是连续的效果了。
a. 滚动layer
滚动的圆圈线是添加一个CAShapeLayer至UIView上,使用Beizier曲线画出来。

- (CAShapeLayer*)shapeLayer {
    if (!_shapeLayer) {
        _shapeLayer = [[CAShapeLayer alloc] init];
        
        _shapeLayer.fillColor = [UIColor clearColor].CGColor;
        _shapeLayer.lineWidth = 3.0f;
        _shapeLayer.strokeColor = [UIColor orangeColor].CGColor;
        
        UIBezierPath* path = [[UIBezierPath alloc] init];
        
        [path moveToPoint: CGPointMake(self.speakerView.width / 2, 0)];
        [path addArcWithCenter: CGPointMake(self.speakerView.width / 2,
                                            self.speakerView.height / 2)
                        radius: self.speakerView.width / 2
                    startAngle: - M_PI / 2
                      endAngle: 3 * M_PI / 2
                     clockwise: YES];
        
        _shapeLayer.path = path.CGPath;
        
        _shapeLayer.strokeStart = 0;
        _shapeLayer.strokeEnd = 0;
    }
    
    return _shapeLayer;
}

需要注意旋转的区域是从-π/2 -> 3π/2。
b.NSTimer的控制

    _strokeTimer = [NSTimer scheduledTimerWithTimeInterval: 0.05f
                                                    target: self
                                                  selector: @selector(strokeCircle)
                                                  userInfo: nil
                                                   repeats: YES];
    [[NSRunLoop currentRunLoop] addTimer: _strokeTimer
                                 forMode: NSRunLoopCommonModes];

- (void)strokeCircle {
    self.shapeLayer.strokeEnd += (0.05f / 60);
}

设置的间隔为0.05s

(2)播放效果

播放效果为PlayAudio页面中,点击播放后中间三个横线依次闪现的效果,类似于微信的语音播放效果。
其原理是帧动画。三张图片分别为一横线、二横线和三横线,大小一致。使用UIImageView进行设置。

- (void)setPlayVoiceIVStyle {
    self.playVoiceIV.animationImages = @[UIImageNamed(BG_PLAYVOICE_1),
                                         UIImageNamed(BG_PLAYVOICE_2),
                                         UIImageNamed(BG_PLAYVOICE_3)];
    self.playVoiceIV.animationDuration = 0.8f;
    self.playVoiceIV.animationRepeatCount = 0;
}

在播放和暂停时调用startAnimating和stopAnimating方法进行开始和暂停动画效果。

结束语

文中介绍的较为笼统,在实际的实现过程中更复杂一些,要考虑到用户的操作,如录制过程中退到后台,录制过程中接入电话等情况。另外,语音和文字的同时输入需要界面之间的转换,也要添加相应的逻辑判断。根据实际的需求进行实现。
代码托管至github中。

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

推荐阅读更多精彩内容