iOS 编程:使用 AVFoundation 框架录制和播放音频

原文:iOS Programming 101: Record and Play Audio using AVFoundation Framework

编者按:有网友要求我们写一篇关于录音的教程。本周,我们与 Purple Development 的 Shi Yiqi 和 Raymond 一起给大家介绍 AVFoundation 框架。Yiqi 和 Raymond 是独立的 iOS 开发者,最近他们发布了 Voice Memo Wifi,可以让用户录制语音备忘录并通过 WiFi 分享。

iOS 提供了各种框架让你在应用程序中使用声音。其中有一个可以让你播放和录制音频文件的框架叫做 AVFoundation。在本教程中,我将带领你了解该框架的基础知识,并向你展示如何管理音频播放,以及录音。

为了给大家提供一个实例,我将构建一个简单的音频应用,让用户可以录制和播放音频。我们的主要目的是演示 AVFoundation 框架,所以应用的用户界面非常简单。

AVFoundation 提供了处理音频的简单方法。在本教程中,我们主要处理这两个类。

  • AVAudioPlayer —— 把它看作是一个可以播放声音文件的音频播放器。通过使用该播放器,你可以播放任何时间长度和(在 iOS 中可用的)任何音频格式的声音。
  • AVAudioRecorder —— 一个用于录制音频的音频记录器。

从示例项目开始

首先,创建一个 "Single View Application" 模版的项目,并命名为 "AudioDemo"。为了让你免于设置用户界面和代码框架,你可以从这里下载项目模板

我为你创建了一个简单的用户界面,它只包含三个按钮,包括 "Record"、"Stop"和 "Play"。这些按钮也是用代码链接起来的。

image

添加 AVFoundation 框架

默认情况下,AVFoundation 框架没有捆绑在任何 Xcode 项目中。所以你必须手动添加它。在项目导航栏中,选择 "AudioDemo" 项目,接着选择 TARGETS 下的 "AudioDemo",然后点击 "Build Phases"。展开 "Link Binary with Libraries",点击 "+"按钮,添加 "AVFoundation.framework"。

image

要使用 AVAudioPlayer 和 AVAudioRecorder 这两个类,我们需要在 ViewController.h 中导入:

#import <AVFoundation/AVFoundation.h>

使用 AVAudioRecorder 录制音频

首先,我们来看看如何使用 AVAudioRecorder 来录制音频。在 ViewController.h 中添加 AVAudioRecorderDelegate 协议和 AVAudioPlayerDelegate 协议。我们将在讲解代码的时候对这两个委托进行解释。

@interface ViewController () <AVAudioRecorderDelegate, AVAudioPlayerDelegate>

接下来,在 ViewController.m 中声明 AVAudioRecorderAVAudioPlayer 的实例变量。

@interface ViewController () <AVAudioRecorderDelegate, AVAudioPlayerDelegate>

@property (weak, nonatomic) IBOutlet UIButton *recordButton;
@property (weak, nonatomic) IBOutlet UIButton *stopButton;
@property (weak, nonatomic) IBOutlet UIButton *playButton;

@property (nonatomic, strong) AVAudioRecorder *recorder;
@property (nonatomic, strong) AVAudioPlayer *player;

@end

AVAudioRecorder 类提供了一种在 iOS 中录制声音的简单方法。要使用录音机,你必须准备一些东西:

  • 指定存放声音文件的 URL 路径。
  • 设置音频会话(AVAudioSession)。
  • 配置 audio recorder 的初始状态。

我们将在 ViewController.mviewDidLoad 方法中进行设置,只需要在该方法中编辑以下代码即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 当应用启动时,禁用 Stop/Play 按钮
    [self.stopButton setEnabled:NO];
    [self.playButton setEnabled:NO];
    
    // 设置音频文件
    NSArray *pathComponents = [NSArray arrayWithObjects:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject], @"MyAudioMemo.m4a",nil];
    NSURL *outputFileURL = [NSURL fileURLWithPathComponents:pathComponents];

    // 设置音频会话
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    
    // 定义录音设置项
    NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
    
    [recordSetting setValue:[NSNumber numberWithInt:kAudioFormatMPEG4AAC] forKey:AVFormatIDKey];
    [recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
    [recordSetting setValue:[NSNumber numberWithInt:2] forKey:AVNumberOfChannelsKey];
    
    // 初始化录音器并设置为准备状态
    self.recorder = [[AVAudioRecorder alloc] initWithURL:outputFileURL settings:recordSetting error:nil];
    self.recorder.delegate = self;
    self.recorder.meteringEnabled = YES;
    [self.recorder prepareToRecord];
}

注:为了演示的目的,我们忽略了错误处理。在实际应用中,不要忘记包含适当的错误处理。

在上面的代码中,我们首先定义了用于保存录音的声音文件URL。然后配置音频会话(audio session)。iOS 通过使用音频会话来处理应用程序的音频行为。在启动时,你的应用会自动获得一个音频会话。你可以通过调用 [AVAudioSession sharedInstance] 来获得会话单例,并对其进行配置。在这里,我们告诉 iOS,应用程序使用 AVAudioSessionCategoryPlayAndRecord 类别,可以实现音频输入和输出。关于音频会话的细节我们就不多说了,大家可以查看官方文档了解更多细节。

AVAudioRecorder 使用基于字典的设置进行配置。在第21-25行,我们使用可选的键来配置音频数据格式、采样率和通道数。最后,我们通过调用 prepareToRecord: 方法来启动音频记录器。

注:关于其他设置键,可以参考 AVFoundation 音频设置常量

实现录音按钮

我们已经完成了音频的准备工作。让我们继续实现 "Record" 按钮的动作方法。在进入代码之前,我先解释一下 "Record" 按钮的工作原理。当用户点击 "Record" 按钮时,应用程序将开始录制,按钮文字将改为 "Pause"。如果用户点击暂停按钮,应用程序将暂停录音,直到再次点击 "Record" 按钮。只有当用户点击 "Stop" 按钮时,录音才会停止。

recordButtonTapped: 方法中编辑以下代码:

// 录制/暂停按钮
- (IBAction)recordButtonTapped:(id)sender {
    // 在录制前停止音频播放
    if (self.player.isPlaying) {
        [self.player stop];
    }
    
    if (!self.recorder.isRecording) {
        AVAudioSession *session = [AVAudioSession sharedInstance];
        [session setActive:YES error:nil];
        
        // 开始录音
        [self.recorder record];
        [self.recordButton setTitle:@"Pause" forState:UIControlStateNormal];
    } else {
        
        // 停止录音
        [self.recorder pause];
        [self.recordButton setTitle:@"Record" forState:UIControlStateNormal];
    }
    
    [self.stopButton setEnabled:YES];
    [self.playButton setEnabled:NO];
}

在上面的代码中,我们首先检查音频播放器是否正在播放中。如果音频播放器正在播放,我们只需使用 stop: 方法停止它。上述代码的第 7 行确定应用程序是否处于录音模式。如果不在录音模式下,应用程序就会激活音频会话并开始录音。为了让录音工作(或声音播放),你的音频会话必须处于激活(active)状态。

通常来说,你可以使用 AVAudioRecorder 类的以下方法来控制录音行为:

  • record - 开始/恢复录音
  • pause - 暂停录音
  • stop - 停止录音

实现停止按钮

对于 "Stop" 按钮,我们只需调用录音器的 stop: 方法,然后停用音频会话。在 stopButtonTapped: 方法中编辑添加以下代码。

// 停止按钮
- (IBAction)stopButtonTapped:(id)sender {
    [self.recorder stop];
    [self.recordButton setTitle:@"Record" forState:UIControlStateNormal];
    
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setActive:NO error:nil];
}

实现 AVAudioRecorderDelegate 协议

你可以利用 AVAudioRecorderDelegate 协议来处理音频中断(比如说,音频录制过程中有一个来电电话)和录制的完成。在本例中,ViewController 遵守此协议。AVAudioRecorderDelegate 协议中定义的方法是可选的。这里,我们只实现 audioRecorderDidFinishRecording: 方法来处理录音的完成。在 ViewController.m 中添加以下代码。

- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag {
    [self.recordButton setTitle:@"Record" forState:UIControlStateNormal];
    
    [self.stopButton setEnabled:NO];
    [self.playButton setEnabled:YES];
}

完成录制后,我们只需将 "Pause" 按钮改回 "Record" 按钮即可。

使用 AVAudioPlayer 播放声音

最后,就到了使用 AVAudioPlayer 实现音频播放的 "Play" 按钮了。在 ViewController.m中,在 playButtonTapped: 方法中编辑添加以下代码:

// 播放按钮
- (IBAction)playButtonTapped:(id)sender {
    if (!self.recorder.isRecording) {
        self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:self.recorder.url error:nil];
        self.player.delegate = self;
        [self.player play];
    }
}

上面的代码非常简单。通常情况下,配置一个音频播放器要做这几件事:

  • 初始化音频播放并指定一个声音文件给它。在本例中,是录音器的音频文件(即 recorder.url)。
  • 指定音频播放器的委托对象,它处理中断以及播放完成事件。
  • 调用 play: 方法来播放声音文件。

实现 AVAudioPlayerDelegate 协议

AVAudioPlayer 对象的委托必须遵守 AVAudioPlayerDelegate 协议。在本例中,它是 ViewController。委托者允许你处理中断、音频解码错误,并在音频播放完毕后更新用户界面。然而,AVAudioPlayerDelegate 协议中的所有方法都是可选的。为了演示它是如何工作的,我们将实现 audioPlayerDidFinishPlaying: 方法来在音频播放完成后显示一个警报提示。其他方法的用法,可以参考AUAudioPlayerDelegate 协议的官方文档。

ViewController.m 中添加以下代码:

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    //  1.实例化UIAlertController对象
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Done" message:@"Finish playing the recording!" preferredStyle:UIAlertControllerStyleAlert];

    //  2.实例化UIAlertAction按钮:确定按钮
    UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
    [alert addAction:defaultAction];

    //  3.显示alertController
    [self presentViewController:alert animated:YES completion:nil];
}

编译并运行应用

你可以使用实际设备或软件模拟器测试音频录制和播放。如果你使用实际设备(如iPhone)测试应用程序,则录制的音频来自于通过内置麦克风或耳机麦克风连接的设备。另外,如果你使用模拟器测试应用程序,音频来自系统偏好设置中的默认音频输入设备。

注:访问麦克风需要访问并获取隐私权限,因此,请在项目的 Info.plist 文件中设置 Privacy - Microphone Usage Description 项。

所以请继续编译并运行该应用吧! 点 "Record" 按钮,开始录制。说点什么,点 "Stop" 按钮,然后选择 "Play" 按钮,收听播放。

image

大家可以从这里下载完整的源码,供大家参考。如果你有什么问题,欢迎给我留言。

本篇文章由来自 Purple Development 的 Yiqi Shi 和 Raymond 贡献。Yiqi 和 Raymond 是独立的 iOS 开发者,最近他们发布了 Voice Memo Wifi,可以让用户录制语音备忘录并通过 WiFi 分享。

附:完整源码

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController () <AVAudioRecorderDelegate, AVAudioPlayerDelegate>

@property (weak, nonatomic) IBOutlet UIButton *recordButton;
@property (weak, nonatomic) IBOutlet UIButton *stopButton;
@property (weak, nonatomic) IBOutlet UIButton *playButton;

@property (nonatomic, strong) AVAudioRecorder *recorder;
@property (nonatomic, strong) AVAudioPlayer *player;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 当应用启动时,禁用 Stop/Play 按钮
    [self.stopButton setEnabled:NO];
    [self.playButton setEnabled:NO];
    
    // 设置音频文件
    NSArray *pathComponents = [NSArray arrayWithObjects:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject], @"MyAudioMemo.m4a",nil];
    NSURL *outputFileURL = [NSURL fileURLWithPathComponents:pathComponents];

    // 设置音频会话
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    
    // 定义录音设置项
    NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
    
    [recordSetting setValue:[NSNumber numberWithInt:kAudioFormatMPEG4AAC] forKey:AVFormatIDKey];
    [recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
    [recordSetting setValue:[NSNumber numberWithInt:2] forKey:AVNumberOfChannelsKey];
    
    // 初始化录音器并设置为准备状态
    self.recorder = [[AVAudioRecorder alloc] initWithURL:outputFileURL settings:recordSetting error:nil];
    self.recorder.delegate = self;
    self.recorder.meteringEnabled = YES;
    [self.recorder prepareToRecord];
}

#pragma mark - Actions

// 录制/暂停按钮
- (IBAction)recordButtonTapped:(id)sender {
    // 在录制前停止音频播放
    if (self.player.isPlaying) {
        [self.player stop];
    }
    
    if (!self.recorder.isRecording) {
        AVAudioSession *session = [AVAudioSession sharedInstance];
        [session setActive:YES error:nil];
        
        // 开始录音
        [self.recorder record];
        [self.recordButton setTitle:@"Pause" forState:UIControlStateNormal];
    } else {
        
        // 停止录音
        [self.recorder pause];
        [self.recordButton setTitle:@"Record" forState:UIControlStateNormal];
    }
    
    [self.stopButton setEnabled:YES];
    [self.playButton setEnabled:NO];
}

// 停止按钮
- (IBAction)stopButtonTapped:(id)sender {
    [self.recorder stop];
    [self.recordButton setTitle:@"Record" forState:UIControlStateNormal];
    
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setActive:NO error:nil];
}

// 播放按钮
- (IBAction)playButtonTapped:(id)sender {
    if (!self.recorder.isRecording) {
        self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:self.recorder.url error:nil];
        self.player.delegate = self;
        [self.player play];
    }
}

#pragma mark - AVAudioRecorderDelegate

- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag {
    [self.recordButton setTitle:@"Record" forState:UIControlStateNormal];
    
    [self.stopButton setEnabled:NO];
    [self.playButton setEnabled:YES];
}

#pragma mark - AVAudioPlayerDelegate

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    //  1.实例化UIAlertController对象
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Done" message:@"Finish playing the recording!" preferredStyle:UIAlertControllerStyleAlert];

    //  2.实例化UIAlertAction按钮:确定按钮
    UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
    [alert addAction:defaultAction];

    //  3.显示alertController
    [self presentViewController:alert animated:YES completion:nil];
}

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

推荐阅读更多精彩内容