iOS 使用AVFoundation实现语音的录制和播放

iOS的AVFoundation框架中的AVAudioRecorder和AVAudioPlayer可以实现语音的录制和播放功能demo下载

AVAudioRecorder

AVAudioRecorder 的初始化方法是

- (nullable instancetype)initWithURL:(NSURL *)url settings:(NSDictionary<NSString *, id> *)settings error:(NSError **)outError;
  • url: 录制的语音文件保存的路径,文件的类型是由这个参数值的file extension推测的。
  • settings: 对audio recored的设置。在iOS7中,默认的设置是
{ AVFormatIDKey = 1819304813;  
AVLinearPCMBitDepthKey = 16;
AVLinearPCMIsBigEndianKey = 0;
AVLinearPCMIsFloatKey = 0; 
AVLinearPCMIsNonInterleaved = 0;
AVNumberOfChannelsKey = 2; 
AVSampleRateKey = 44100;}

下面的三个key适用于所有的语音格式:
AVFormatIDKey:格式的标识,表示音频文件的格式,它对应的是个枚举值。

Use these identifiers to test for the presence of audio codecs on a system. If a given codec is present, you can use its identifier to specify that codec for data encoding or decoding, according to the capabilities of the codec. For more information, see Core Audio Overview.

CF_ENUM(AudioFormatID)
{
    kAudioFormatLinearPCM               = 'lpcm',
    kAudioFormatAC3                     = 'ac-3',
    kAudioFormat60958AC3                = 'cac3',
    kAudioFormatAppleIMA4               = 'ima4',
    kAudioFormatMPEG4AAC                = 'aac ',
    kAudioFormatMPEG4CELP               = 'celp',
    kAudioFormatMPEG4HVXC               = 'hvxc',
    kAudioFormatMPEG4TwinVQ             = 'twvq',
    kAudioFormatMACE3                   = 'MAC3',
    kAudioFormatMACE6                   = 'MAC6',
    kAudioFormatULaw                    = 'ulaw',
    kAudioFormatALaw                    = 'alaw',
    kAudioFormatQDesign                 = 'QDMC',
    kAudioFormatQDesign2                = 'QDM2',
    kAudioFormatQUALCOMM                = 'Qclp',
    kAudioFormatMPEGLayer1              = '.mp1',
    kAudioFormatMPEGLayer2              = '.mp2',
    kAudioFormatMPEGLayer3              = '.mp3',
    kAudioFormatTimeCode                = 'time',
    kAudioFormatMIDIStream              = 'midi',
    kAudioFormatParameterValueStream    = 'apvs',
    kAudioFormatAppleLossless           = 'alac',
    kAudioFormatMPEG4AAC_HE             = 'aach',
    kAudioFormatMPEG4AAC_LD             = 'aacl',
    kAudioFormatMPEG4AAC_ELD            = 'aace',
    kAudioFormatMPEG4AAC_ELD_SBR        = 'aacf',
    kAudioFormatMPEG4AAC_ELD_V2         = 'aacg',    
    kAudioFormatMPEG4AAC_HE_V2          = 'aacp',
    kAudioFormatMPEG4AAC_Spatial        = 'aacs',
    kAudioFormatAMR                     = 'samr',
    kAudioFormatAMR_WB                  = 'sawb',
    kAudioFormatAudible                 = 'AUDB',
    kAudioFormatiLBC                    = 'ilbc',
    kAudioFormatDVIIntelIMA             = 0x6D730011,
    kAudioFormatMicrosoftGSM            = 0x6D730031,
    kAudioFormatAES3                    = 'aes3',
    kAudioFormatEnhancedAC3             = 'ec-3'
};

AVSampleRateKey: 抽样率,单位时间内的抽样数。44.1kHZ和标准的CD Audio是相同的,除非你需要一个高保真的录音,你不需要这样高的采样率,大部分的音频软件只能特定的速率像32KHZ,24KHZ,16KHZ,12KHZ.8KHZ是电话采样率,对一般的录音已经足够了。
AVNumberOfChannelsKey:通道数。设成2的话是双声道。iPhone只有一个麦克风,一个单声道的通道足够了,它把你的数据需求削减了一半。
AVLinearPCMBitDepthKey:位宽。抽样后的数值用二进制表示,这个值表示二进制的位数。值可以是 8, 16, 24, or 32。
参考

开始录音的代码:


    NSString * url = NSTemporaryDirectory();
    url = [url stringByAppendingString:[NSString stringWithFormat:@"%f.wav", [[NSDate date] timeIntervalSince1970]]];
    NSMutableDictionary * settings = @{}.mutableCopy;
    [settings setObject:[NSNumber numberWithFloat:8000.0] forKey:AVSampleRateKey];
    [settings setObject:[NSNumber numberWithInt: kAudioFormatLinearPCM] forKey:AVFormatIDKey];  
    [settings setObject:@1 forKey:AVNumberOfChannelsKey];//设置成一个通道,iPnone只有一个麦克风,一个通道已经足够了
    [settings setObject:@16 forKey:AVLinearPCMBitDepthKey];//采样的位数
    self.audioRecorder = [[AVAudioRecorder  alloc] initWithURL:[NSURL fileURLWithPath:url] settings:settings error:&error];
    self.audioRecorder.delegate = self;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:nil];
    self.audioRecorder.meteringEnabled = YES;
    BOOL success = [self.audioRecorder record];
    if (success) {
        NSLog(@"录音开始成功");
    }else{
        NSLog(@"录音开始失败");
    }

遇到的问题

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:nil];

这个是设置AVAudioSession的category的,如果不设置的话,在模拟器上success是YES,但是在真机上是NO。同样,在用AVAudioPlayer播放语音时要设置category为AVAudioSessionCategoryPlayback

如果把setting里的AVFormatIDKey值改为kAudioFormatMPEGLayer3,则url的后缀也要改成.mp3,否则初始化AVAudioRecorder实例会失败。

指定的音频格式一定要和文件写入的URL文件类型保持一致。如果录制.wav文件格式,AVFormatIDKey指定的值不是kAudioFormatLinearPCM则会发生错误。NSError 会返回如下错误
The operation couldn’t be completed. (OSState error 1718449215.)

iOS 4.3以后不支持amr格式的录制和播放了,所以使用kAudioFormatAMR因为无法录制amr格式的语音。

停止录音

    [self.audioRecorder stop];

调用这个方法后,会走下面的代理方法

-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{
    NSURL * url = recorder.url;
}

获取录音过程中的分贝

要在录音过程中获取分贝数,要在录音前先把AVAudioRecorder的属性meteringEnabled设置成YES.
在录音开始后,可以设置一个定时器获取分贝数

_metesTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(setVoiceImage) userInfo:nil repeats:YES];
-(void)setVoiceImage{
    if (self.audioRecorder.isRecording) {
        [self.audioRecorder updateMeters];
        float peakPower = [self.audioRecorder peakPowerForChannel:0];
        NSLog(@"%f", peakPower);
    }
}

AVAudioRecorderaveragePowerForChannelpeakPowerForChannel方法返回的是分贝数据,数值在-160 – 0之间

播放语音

可以将录音后获取的url传入下面的方法中:

- (void)playAudioWithURL:(NSURL *)url{
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
    NSError * error;
    self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
    self.audioPlayer.delegate = self;
    BOOL success = [self.audioPlayer play];
    if (success) {
        NSLog(@"播放成功");
    }else{
        NSLog(@"播放失败");
    }
}

我在demo里封装了一个ZMAudioManager的类,方便进行语音的播放和录制:
开始录音

[[ZMAudioManager shareInstance] startRecordingWithFileName:[NSString stringWithFormat:@"%f.wav", [[NSDate date] timeIntervalSince1970]] completion:^(NSError *error) {
    if (error) {

    else{

    }
}];

停止录音,recordPath是录音文件存放的路径,aDuration是录音时长

 [[ZMAudioManager  shareInstance] stopRecordingWithType:ZMAudioRecordeAMRType completion:^(NSString *recordPath, NSInteger aDuration, NSError *error) {
        if (error) {
            UIAlertView *a = [[UIAlertView alloc] initWithTitle:@"error" message:error.domain delegate:nil cancelButtonTitle:@"确定" otherButtonTitles: nil];
            [a show];
        }else{
           
 }];

播放录音,audioPath是录音文件存放的路径,录音播放完后会走completion回调

 [[ZMAudioManager shareInstance] playAudioWithPath:audioPath completion:^(NSError *error) {
 
  }];

在实际开发中,考虑到要和android之间通信,android支持amr,不支持wav;iOS支持wav,不支持amr。通常iOS客户端会进行amr和wav的互相转换。
在demo里EMVoiceConverter文件包含了wav和amr互相转换的方法:

+ (int)amrToWav:(NSString*)_amrPath wavSavePath:(NSString*)_savePath;

+ (int)wavToAmr:(NSString*)_wavPath amrSavePath:(NSString*)_savePath;

基础知识

声音是物体震动发出的声波。声音的频率是值单位时间内(每秒钟)物体震动的次数,或者说频率是每秒经过一给定点的声波数量。
人耳所能听到的声音,最低的频率是从20Hz起一直到最高频率20KHZ,因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,只有采样频率高于声音信号最高频率的两倍时,才能把数字信号表示的声音还原成为原来的声音,所以音频文件的采样率一般在40~50KHZ,比如最常见的CD音质采样率44.1KHZ。

音频的采集过程主要通过设备将环境中的模拟信号采集成 PCM (Pulse Code Modulation)(脉冲编码调制)编码的原始数据,然后编码压缩成 MP3 等格式的数据分发出去。常见的音频压缩格式有:MP3,AAC,OGG,WMA,Opus,FLAC,APE,m4a 和 AMR 等

PCM编码

PCM编码主要过程是将话音、图像等模拟信号每隔一定时间进行取样,使其离散化,同时将抽样值按分层单位四舍五入取整量化,同时将抽样值按一组二进制码来表示抽样脉冲的幅值。
也就是说,PCM对模拟信号进行了抽样、量化和编码三个过程。
PCM数据是最原始的音频数据完全无损,所以PCM数据虽然音质优秀但体积庞大,为了解决这个问题先后诞生了一系列的音频格式,这些音频格式运用不同的方法对音频数据进行压缩,其中有无损压缩(ALAC、APE、FLAC)和有损压缩(MP3、AAC、OGG、WMA)两种。
目前最为常用的音频格式是MP3,MP3是一种有损压缩的音频格式,设计这种格式的目的就是为了大幅度的减小音频的数据量,它舍弃PCM音频数据中人类听觉不敏感的部分,

  • 抽样频率(Sampling Rate):单位时间内采集的样本数。采样频率必须至少是信号中最大频率分量频率的两倍,否则就不能从信号采样中恢复原始信号,这其实就是著名的香农采样定理。CD音质采样率为 44.1 kHz,其他常用采样率:22.05KHz,11.025KHz,一般网络和移动通信的音频采样率:8KHz。声音的频率是每秒经过一给定点的声波数量。

  • 位宽(bit depth):
    每一个采样点都需要用一个数值来表示大小,这个数值的数据类型大小可以是:4bit、8bit、16bit、32bit 等等,位数越多,表示得就越精细,声音质量自然就越好,而数据量也会成倍增大。我们在音频采样过程中常用的位宽是 8bit 或者 16bit;

  • 声道数(channels):
    由于音频的采集和播放是可以叠加的,因此,可以同时从多个音频源采集声音,并分别输出到不同的扬声器,故声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。声道数为 1 和 2 分别称为单声道和双声道,是比较常见的声道参数;

  • 音频帧(frame):
    音频跟视频很不一样,视频每一帧就是一张图像,而从上面的正玄波可以看出,音频数据是流式的,本身没有明确的一帧帧的概念,在实际的应用中,为了音频算法处理/传输的方便,一般约定俗成取 2.5ms~60ms 为单位的数据量为一帧音频。这个时间被称之为“采样时间”,其长度没有特别的标准,它是根据编解码器和具体应用的需求来决定的
    根据以上定义,我们可以计算一下一帧音频帧的大小。
    假设某音频信号是采样率为 8kHz、双通道、位宽为 16bit,20ms 一帧,则一帧音频数据的大小为:
    1size = 8000 x 2 x 16bit x 0.02s = 5120 bit = 640 byte

  • 比特率(bit rate):表示经过编码(压缩)后的音频数据每秒钟需要用多少个比特来表示,单位常为kbps。

常见的音频编码格式

AVAudioSession

Audio Session category behavior
Responding to Audio Session Interruptions

如果你在音乐应用中播放音乐并收到电话或FaceTime请求时,应用的音频播放会暂停。如果拒绝来电或请求,则控制返回到应用程序,音频再次开始播放。
你可以直接观察被AVAudioSession发出的终端通知:

- (void)setupNotifications{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:nil];
}
- (void)handleInterruption:(NSNotification *)notification{
    if (notification.userInfo[AVAudioSessionInterruptionTypeKey]) {
        AVAudioSessionInterruptionType type =  [notification.userInfo[AVAudioSessionInterruptionTypeKey] intValue];
        if (type == AVAudioSessionInterruptionTypeBegan) {
            //中断开始
        }else if (type == AVAudioSessionInterruptionTypeEnded){
            //中断结束
            if (notification.userInfo[AVAudioSessionInterruptionOptionKey]) {
                AVAudioSessionInterruptionOptions option = [notification.userInfo[AVAudioSessionInterruptionOptionKey] intValue];
                if (option == AVAudioSessionInterruptionOptionShouldResume) {
                    //中断结束,播放将恢复
                }else{
                    //中断结束,播放不会结束
                }
            }
        }
    }    
}
Observe for Route Change Notifications

AVAudioSession的一项重要职责是管理audio route changes。将音频输入或输出添加到iOS设备或从iOS设备中移除时, route change 会发生。route change的行为包括插入一副耳机,连接蓝牙耳机或拔下USB音频接口。当放生这些route change时,AVAudioSession将相应地重新传送音频信号,并向任何注册的观察者发送包含更改细节的通知。

- (void)setupNotifications{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionRouteChangeNotification object:nil];
}

- (void)handleInterruption:(NSNotification *)notification{
    BOOL headphonesConnected;
    if (notification.userInfo[AVAudioSessionRouteChangeReasonKey]) {
        AVAudioSessionRouteChangeReason reason =  [notification.userInfo[AVAudioSessionRouteChangeReasonKey] intValue];
        switch (reason) {
            //插入耳机
            case AVAudioSessionRouteChangeReasonNewDeviceAvailable:{
                AVAudioSession *session = [AVAudioSession sharedInstance];
                for (AVAudioSessionPortDescription * output in session.currentRoute.outputs) {
                    if (output.portType == AVAudioSessionPortHeadphones) {
                        headphonesConnected = YES;
                        break;
                    }
                }
            }
            break;
           //拔出耳机
            case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:{
                AVAudioSessionRouteDescription * previousRoute = notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
                for (AVAudioSessionPortDescription * output in previousRoute.outputs) {
                    if (output.portType == AVAudioSessionPortHeadphones) {
                        headphonesConnected = false;
                        break;
                    }
                }
            }
            break;
            default:
                break;
        }
    }
}

active

setActive: withOptions:error:nil
当你的app deactive自己的AudioSession时系统会通知上一个被中断播放app中断已经结束。如果你的app在deactive时传入了AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation参数,那么其他app在接到打断结束回调时会多得到一个参数kAudioSessionInterruptionType_ShouldResume否则就是AVAudioSessionInterruptionOptionShouldResume,根据参数的值可以决定是否继续播放。
大概流程是这样的:

  • 一个音乐软件A正在播放;
  • 用户打开你的软件播放对话语音,AudioSession active;
  • 音乐软件A音乐被打断并收到InterruptBegin事件;
    对话语音播放结束,AudioSession deactive并且传入NotifyOthersOnDeactivation参数;
  • 音乐软件A收到InterruptEnd事件,查看Resume参数,如果是ShouldResume控制音频继续播放,如果是ShouldNotResume就维持打断状态;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容