iOS-音频开发

一、音效播放

AudioToolbox.framework是一套基于C语言的框架,使用它来播放音效其本质是将短音频注册到系统声音服务(System Sound Service)。System Sound Service是一种简单、底层的声音播放服务,但是它本身也存在着一些限制:

音频播放时间不能超过30s
数据必须是PCM或者IMA4格式
音频文件必须打包成.caf、.aif、.wav中的一种(注意这是官方文档的说法,实际测试发现一些.mp3也可以播放)
使用System Sound Service 播放音效的步骤如下:

  1. 调用AudioServicesCreateSystemSoundID( CFURLRef inFileURL, SystemSoundID outSystemSoundID)函数获得系统声音ID。*
  2. 如果需要监听播放完成操作,则使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID, CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void inClientData)方法注册回调函数(个人觉得用起来麻烦,可以用其block回调)*

3.调用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者带有震动效果)。

直接上代码:
首先要导入:AudioToolbox/AudioToolbox.h
#import <AudioToolbox/AudioToolbox.h>

- (void)playSoundEffect:(NSString*)name {

//获取文件的路径
NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:name ofType:nil];

NSURL *fileUrl = [NSURL fileURLWithPath:audioFilePath];

//1.获取系统声音ID
SystemSoundID soundID = 0;

AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileUrl), &soundID);

//2.如何播放完之后需要执行某些操作,可以调用下面方法
AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL,soundCompleteCallBack, NULL);

//3.播放音频
AudioServicesPlaySystemSound(soundID);

// 这个方法直接监听播放完成回调,方便了很多
AudioServicesPlaySystemSoundWithCompletion(soundID, ^{
    
    
    NSLog(@"播放完成回调");
});


AudioServicesPlayAlertSound(soundID); // 播放并且震动
}

播放完成回调方法

    /**
     *  播放完成的回调函数
     *
     *  @param soundID    系统声音ID
     *  @param clientDate 回调时传递的数据
 */
void soundCompleteCallBack(SystemSoundID soundID,void* clientDate) {

NSLog(@"播放完成");
}

在触摸控制器的时候调用改方法

  - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event  {

[self playSoundEffect:@"test.wav"];
}

二、音乐播放

音乐播放可以用<AVFoundation/AVFoundation.h>中的
AVAudioPlayer,首先看下这个类有哪些属性和方法。


属性 说明
@property(readonly, getter=isPlaying) BOOL playing 是否正在播放,只读
@property(readonly) NSUInteger numberOfChannels 音频声道数,只读
@property(readonly) NSTimeInterval duration 音频时长,只读
@property(readonly) NSURL *url 音频文件路径,只读
@property(readonly) NSData *data 音频数据,只读
@property float pan 立体声平衡,如果为-1.0则完全左声道,如果0.0则左右声道平衡,如果为1.0则完全为右声道
@property float volume 音量大小,范围0-1.0
@property BOOL enableRate 是否允许改变播放速率
@property float rate 播放速率,范围0.5-2.0,如果为1.0则正常播放,如果要修改播放速率则必须设置enableRate为YES
@property NSTimeInterval currentTime 当前播放时长
@property(readonly) NSTimeInterval deviceCurrentTime 输出设备播放音频的时间,注意如果播放中被暂停此时间也会继续累加
@property NSInteger numberOfLoops 循环播放次数,如果为0则不循环,如果小于0则无限循环,大于0则表示循环次数
@property(readonly) NSDictionary *settings 音频播放设置信息,只读
@property(getter=isMeteringEnabled) BOOL meteringEnabled 是否启用音频测量,默认为NO,一旦启用音频测量可以通过updateMeters方法更新测量值


** 对象方法 说明**

  • (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError 使用文件URL初始化播放器,注意这个URL不能是HTTP URL,AVAudioPlayer不支持加载网络媒体流,只能播放本地文件
  • (instancetype)initWithData:(NSData *)data error:(NSError **)outError 使用NSData初始化播放器,注意使用此方法时必须文件格式和文件后缀一致,否则出错,所以相比此方法更推荐使用上述方法或- (instancetype)initWithData:(NSData *)data fileTypeHint:(NSString *)utiString error:(NSError **)outError方法进行初始化
  • (BOOL)prepareToPlay; 加载音频文件到缓冲区,注意即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法。
  • (BOOL)play; 播放音频文件
  • (BOOL)playAtTime:(NSTimeInterval)time 在指定的时间开始播放音频
  • (void)pause; 暂停播放
  • (void)stop; 停止播放
  • (void)updateMeters 更新音频测量值,注意如果要更新音频测量值必须设置meteringEnabled为YES,通过音频测量值可以即时获得音频分贝等信息
  • (float)peakPowerForChannel:(NSUInteger)channelNumber; 获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
  • (float)averagePowerForChannel:(NSUInteger)channelNumber 获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法
    @property(nonatomic, copy) NSArray *channelAssignments 获得或设置播放声道

代理方法 说明

  • (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag 音频播放完成
  • (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError )error 音频解码发生错误/

看完这个类后,我制作一个简单音乐播放器,效果如下。


musicPlayer.gif

界面我是在main.storyBoard中简单拖的,主要看核心的实现代码。
首先导入框架
#import <AVFoundation/AVFoundation.h>

@property (weak, nonatomic) IBOutlet UILabel *musicName;
@property (weak, nonatomic) IBOutlet UILabel *panLabel; // 声道
@property (weak, nonatomic) IBOutlet UILabel *playTimeLabel; //总时间,单位s
@property (weak, nonatomic) IBOutlet UISlider *volumeSlider; // 音量控制
@property (weak, nonatomic) IBOutlet UISlider *rateSlider; // 播放速度控制
@property (weak, nonatomic) IBOutlet UILabel *meterLabel; // 分贝数值
@property (weak, nonatomic) IBOutlet UIProgressView *progressView; // 播放进度条

@property(nonatomic, strong) AVAudioPlayer *musicPlayer; // 音乐播放器
@property(nonatomic, strong) NSTimer *timer; // 定时器(用来更新进度条)
@property(nonatomic, strong) UIView *meterView; // 显示分贝视图

初始化播放器
- (AVAudioPlayer*)musicPlayer {

if (_musicPlayer == nil) {
    
    
    //1.获取文件的路径
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test.mp3" ofType:nil];
    
    NSURL *fileUrl = [NSURL fileURLWithPath:filePath];
    
    NSError *error = nil;
    _musicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:&error];
    
    // 可以改变播放速度
    _musicPlayer.enableRate = YES;
    
    // 可以检测分贝
    _musicPlayer.meteringEnabled = YES;
    
    //
    _musicPlayer.numberOfLoops = 0;
    
    _musicPlayer.delegate = self;
    
    [_musicPlayer prepareToPlay]; // 加载音频文件到缓存
    
    if (error) {
        
        NSLog(@"初始化出错%@",error.localizedDescription);
        return nil;
    }
    
//为了支持后台播放
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
    [audioSession setActive:YES error:nil];
    
    // 添加通知,拔出耳机后暂停播放
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];
    
    
    
}
return _musicPlayer;
}


- (NSTimer*)timer {

if (_timer == nil) {
    
    
    
    _timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateInfo) userInfo:nil repeats:YES];
}
return _timer;
}

- (UIView*)meterView {

if (_meterView == nil) {
    
    
    _meterView = [[UIView alloc] init];
    
    _meterView.backgroundColor = [UIColor greenColor];
    
}
return _meterView;
}

**注意:
1.为了能够支持后台播放需要info.plist中.添加Required background modes,并且设置item 0=App plays audio or streams audio/video using AirPlay(其实可以直接通过Xcode在Project Targets-Capabilities-Background Modes中设置)
2.同时也为了能效应远程事件(一般就是耳机控制咯)

(void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0);事件。要监听到这个事件有三个前提(视图控制器UIViewController或应用程序UIApplication只有两个) 启用远程事件接收(使用[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];方法)。 对于UI控件同样要求必须是第一响应者(对于视图控制器UIViewController或者应用程序UIApplication对象监听无此要求)。 应用程序必须是当前音频的控制者,也就是在iOS 7中通知栏中当前音频播放程序必须是我们自己开发程序
**
所有我们添加如下代码

- (void)viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];



[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}


- (void)viewDidDisappear:(BOOL)animated {

[super viewDidDisappear:animated];

[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
}

这个是定时器更新进度条和分贝的方法

- (void)updateInfo {

//更新进度条
float progress = self.musicPlayer.currentTime/self.musicPlayer.duration;

[self.progressView setProgress:progress animated:YES];

// 跟新分贝
[self.musicPlayer updateMeters];
    // - (float)peakPowerForChannel:(NSUInteger)channelNumber;  获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
  //    - (float)averagePowerForChannel:(NSUInteger)channelNumber   获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法

float meter = [self.musicPlayer averagePowerForChannel:1];

self.meterLabel.text = [NSString stringWithFormat:@"%f",meter];

CGFloat newWith = 320*(meter+60)/60;

self.meterView.frame = CGRectMake(0, CGRectGetMaxY(self.meterLabel.frame)+5, newWith, 20);

}

各种响应事件方法

pragma mark - 响应事件

// 控制播放进度
- (void)dealTap:(UITapGestureRecognizer*)tap {


// 获取手势在进度条上的点
CGPoint point = [tap locationInView:self.progressView];

// 设置当前播放的时间
self.musicPlayer.currentTime = self.musicPlayer.duration*(point.x/self.progressView.frame.size.width);


}

// 暂停
- (IBAction)pause:(id)sender {

if ([self.musicPlayer isPlaying]) {
    
    [self.musicPlayer pause];
    
    self.timer.fireDate = [NSDate distantFuture]; // 暂停定时器
}
}


// 播放
- (IBAction)play:(id)sender {

if (![self.musicPlayer isPlaying]) {
    
    [self.musicPlayer play];
    
    self.timer.fireDate = [NSDate distantPast]; // 恢复定时器
}
}

// 停止
- (IBAction)stop:(id)sender {

if ([self.musicPlayer isPlaying]) {
    
    [self.musicPlayer stop];
//        [self.timer invalidate];
    }
 }

// 改变音量
- (IBAction)volumeChange:(id)sender {

UISlider *volumeSwitch = (UISlider*)sender;

self.musicPlayer.volume = volumeSwitch.value;
}

// 改变播放速度
- (IBAction)rateChage:(id)sender {

UISlider *rateSwitch = (UISlider*)sender;

self.musicPlayer.rate = rateSwitch.value;
  }
远程控制事件
// 远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {

NSLog(@"测试哦");

NSLog(@"%d",event.type);

if (event.type==UIEventTypeRemoteControl) {
    
    
    NSLog(@"%d",event.subtype);
    
    switch (event.subtype) {
        case UIEventSubtypeRemoteControlPlay:
            [self play:nil];
            
            NSLog(@"播放");
            
            break;
        case UIEventSubtypeRemoteControlTogglePlayPause : // 耳机中间点击一下
            
            if([self.musicPlayer isPlaying]){
            
                  [self pause:nil];
            }
            else {
            
                [self play:nil];
            }
          
            NSLog(@"暂停");
            break;
            
        default:
            break;
    }
}


/*typedef NS_ENUM(NSInteger, UIEventSubtype) {
 // 不包含任何子事件类型
 UIEventSubtypeNone                              = 0,
 
 // 摇晃事件(从iOS3.0开始支持此事件)
 UIEventSubtypeMotionShake                       = 1,
 
 //远程控制子事件类型(从iOS4.0开始支持远程控制事件)
 //播放事件【操作:停止状态下,按耳机线控中间按钮一下】
 UIEventSubtypeRemoteControlPlay                 = 100,
 //暂停事件
 UIEventSubtypeRemoteControlPause                = 101,
 //停止事件
 UIEventSubtypeRemoteControlStop                 = 102,
 //播放或暂停切换【操作:播放或暂停状态下,按耳机线控中间按钮一下】
 UIEventSubtypeRemoteControlTogglePlayPause      = 103,
 //下一曲【操作:按耳机线控中间按钮两下】
 UIEventSubtypeRemoteControlNextTrack            = 104,
 //上一曲【操作:按耳机线控中间按钮三下】
 UIEventSubtypeRemoteControlPreviousTrack        = 105,
 //快退开始【操作:按耳机线控中间按钮三下不要松开】
 UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
 //快退停止【操作:按耳机线控中间按钮三下到了快退的位置松开】
 UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
 //快进开始【操作:按耳机线控中间按钮两下不要松开】
 UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
 //快进停止【操作:按耳机线控中间按钮两下到了快进的位置松开】
 UIEventSubtypeRemoteControlEndSeekingForward    = 109,
 };*/


}
监听耳机拔出来的通知方法(耳机拨出来暂停播放)
// 耳机拨出来的通知
- (void)routeChange:(NSNotification*)notice {

NSDictionary *dic = notice.userInfo;

int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];

//等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示旧输出不可用
if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
    
    AVAudioSessionRouteDescription *routeDescription = dic[AVAudioSessionRouteChangePreviousRouteKey];
    
    AVAudioSessionPortDescription *portDescription = [routeDescription.outputs firstObject];
    //原设备为耳机则暂停
    
    if ([portDescription.portType isEqualToString:@"Headphones"]) {
        
        [self pause:nil];
    }
}
}
最后为了是点击进度条能够改变播放进度,给进度条添加一个tap手势
//给进度条添加一个手势,控制其播放进度

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dealTap:)];

[self.progressView addGestureRecognizer:tap];

控制播放进度的方法

// 控制播放进度
- (void)dealTap:(UITapGestureRecognizer*)tap {


// 获取手势在进度条上的点
CGPoint point = [tap locationInView:self.progressView];

// 设置当前播放的时间
self.musicPlayer.currentTime = self.musicPlayer.duration*(point.x/self.progressView.frame.size.width);


}

结尾:

上面基本实现了简单音乐播放器,还是比较简单的。但还不够完善,比如不能上一曲和下一曲播放,另外这个也不支持在线音频的播放,这些问题留着下篇再讲吧!

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

推荐阅读更多精彩内容