音频初见

音频初见

基本介绍

音频: 从形式上分为
短音频(音效播放):不需要进度|循环等控制 AudioToolbox.framework
(C语言框架

  • 本质:将音频注册到system sound service(简单底层的声音播放服务))
    特点:
  • 播放时间不超过30s
  • 数据格式PCM/IMA4
  • 音频文件必须打包成.caf,.aif,.wav,(.MP3也可以)

长音频(音乐播放)

  • 较大的音频文件,需要进行精确的控制的场景
  • AVFoundation.framework
  • 支持的音频格式有:AAC,ALAC,HE-AAC,iLBC,IMA4,MP3等.

音效播放

步骤:

  • 不要忘记导入头文件: #import \<AudioToolbox/AudioToolbox.h>
  • 1.调用AudioServicesCreateSystemSoundID(CFURLRef inFileURL, SystemSoundID* outSystemSoundID)函数获得系统声音ID。
  • 2.如果需要监听播放完成操作,则使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法注册回调函数。
  • 3.调用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID)
    AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者带有震动效果)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
- (void)audioBox{

//获取资源路径
NSString *pathSource = [[NSBundle mainBundle]pathForResource:@"audiobox" ofType:@".caf"];
NSURL *sourceUrl = [NSURL fileURLWithPath:pathSource];

//系统声音id
SystemSoundID soundID = 0;
/**
注册音效服务

@param inFileURL#> 音频文件URL 需要桥接->C语言
@param outSystemSoundID#> 输出音效ID
@return 将音效文件加入到系统音频服务中并返回一个长整型音频ID
*/
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(sourceUrl), &soundID);



/**
添加播放完毕回调函数

@param inSystemSoundID#> 音效ID
@param inRunLoop#> 添加到Runloop
@param inRunLoopMode#> RunloopMode
@param inCompletionRoutine#> 回调函数
@param inClientData#> 传递给回调函数的相关数据
@return ..
*/
AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, playCompletion, NULL);


//播放
AudioServicesPlayAlertSound(soundID); //播放音效并震动
// AudioServicesPlaySystemSound(soundID); //播放音效
}


/**
回调函数

@param ssID 音效id
@param clientData 回调时传递的数据
*/
void playCompletion(SystemSoundID ssID, void* __nullable clientData){
NSLog(@"播放完成");
//摧毁音效
AudioServicesDisposeSystemSoundID(ssID);
}

音乐播放 AVAudioPlayer

AVAudioPlayer支持多种音频格式,而且能够对进度,音量,播放速度等控制
头文件 #import \<AVFoundation/AVFoundation.h>
步骤:

  • 1.给定资源路径,或者二进制数据初始化播放器
  • 2.设置相关属性
  • 3.执行播放,更新相关UI(进度更新,歌词显示(CADislinkplay/NSTimer))

初始化方法

AVAudioPlayer可以接受本地资源路径(NSURL)/NSData类型数据(可以用来加载网络链接),但是实质上AVAudioPlayer不支持流式播放,只能播放完整的文件,使用后者加载网络链接,当播放的音频文件较大或者网络不好就需要过长的时间等待
每一个实例化方法返回一个AVAudioPlayer对象,每一个文件/Data对应一个AVAudioPlayer对象,传入一个NSError的指针,用于判断初始化是否成功.

1
2
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)outError;

播放控制相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//准备播放,分配播放所需的资源,并将其加入内部播放队列,加载音频文件到缓冲区,注意:即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法。
- (BOOL)prepareToPlay;
//播放,如果资源还没准备好,会隐式调用prepareToPlay方法,是异步的
- (BOOL)play;
//相对当前设备时间或指定的时间开始播放音频
- (BOOL)playAtTime:(NSTimeInterval)time

NSTimeInterval nowTime = self.audioPlayer.deviceCurrentTime;
[self.audioPlayer playAtTime:nowTime + 2.0];

//暂停播放 并且准备好继续播放
- (void)pause; /* pauses playback, but remains ready to play. */
//停止播放 不再准备好继续播放,再次调用会重新开始播放
- (void)stop; /* stops playback. no longer ready to play. */

属性相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//获取是否正在播放
@property(readonly, getter=isPlaying) BOOL playing;
//获取当前音频声道数
@property(readonly) NSUInteger numberOfChannels;
//获取当前音频时长
@property(readonly) NSTimeInterval duration;


//获取创建时的音频路径 跟初始化方法相关联
@property(readonly, nullable) NSURL *url;
//获取创建时的音频数据 跟初始化方法相关联
@property(readonly, nullable) NSData *data;
//声道 -1.0~0.0左声道 0.0临界值 0.0~1.0为右声道
@property float pan
//音量 正常范围在0.0~1.0
@property float volume;
- (void)setVolume:(float)volume fadeDuration:(NSTimeInterval)duration NS_AVAILABLE(10_12, 10_0); /* fade to a new volume over a duration */

//是否可以改变播放速率 必须在prepareToPlay方法前调用
@property BOOL enableRate
//播放速率 1.0为正常速率 0.5一半 2.0双倍速播放
@property float rate

//当前播放的时间点,可以实现定点进行播放
@property NSTimeInterval currentTime;

//输出设备播放音频的时间,注意:如果播放中被暂停此时间也会继续累加
@property(readonly) NSTimeInterval deviceCurrentTime

//设置音频播放循环次数 如果为0则不循环,如果小于0则无限循环,大于0则表示循环次数
@property NSInteger numberOfLoops;

//获取音频设置字典
@property(readonly) NSDictionary<NSString *, id> *settings NS_AVAILABLE(10_7, 4_0); /* returns a settings dictionary with keys as described in AVAudioSettings.h */

/* returns the format of the audio data */
@property(readonly) AVAudioFormat *format NS_AVAILABLE(10_12, 10_0);


//是否开启仪表计数功能(启用音频测量) 默认为NO,一旦启用音频测量可以通过updateMeters方法更新测量值
@property(getter=isMeteringEnabled) BOOL meteringEnabled; /* turns level metering on or off. default is off. */

//更新仪表计数的值
- (void)updateMeters; /* call to refresh meter values */

//获得指定声道的分贝峰值,如果要获得分贝峰值必须在此之前调用updateMeters方法
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */

//获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */


/* The channels property lets you assign the output to play to specific channels as described by AVAudioSession's channels property */
/* This property is nil valued until set. */
/* The array must have the same number of channels as returned by the numberOfChannels property. */
//获得或设置播放声道
@property(nonatomic, copy, nullable) NSArray<AVAudioSessionChannelDescription *> *channelAssignments

代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!--音频播放结束后调用的函数 被中断停止播放并不会被调用--> 
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag;


<!--播放遇到错误时调用的函数-->
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error;


/**
音乐播放器被打断

@param player .
*/
-(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player{
NSLog(@"播放被中断");
//如果音频被中断,比如有电话呼入,该方法就会被回调,该方法可以保存当前播放信息,以便恢复继续播放的进度
}
/**
* 音乐播放器停止打断
*
* @param player .
*/
-(void)audioPlayerEndInterruption:(AVAudioPlayer *)player
{
NSLog(@"audioPlayerEndInterruption-停止打断");
[player play];
//继续播放
}

后台播放

1.需要添加info.plist
1

1
2
3
4
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>

或者
2
2.添加代码支持

1
2
3
//设置锁屏仍能继续播放
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive: YES error: nil];

监听耳机事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//添加通知,拔出耳机后暂停播放
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];

/**
* 一旦输出改变则执行此方法
*
* @param notification 输出改变通知对象
*/
-(void)routeChange:(NSNotification *)notification{

NSDictionary *dic=notification.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.audioPlayer pause];
}
}
}

后台播放信息显示

1.导入框架 #import \<MediaPlayer/MediaPlayer.h>
2.添加代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
//设置歌曲题目
[dict setObject:@"题目" forKey:MPMediaItemPropertyTitle];
//设置歌手名
[dict setObject:@"歌手" forKey:MPMediaItemPropertyArtist];
//设置专辑名
[dict setObject:@"专辑" forKey:MPMediaItemPropertyAlbumTitle];
//设置显示的图片
UIImage *newImage = [UIImage imageNamed:@"43.png"];
[dict setObject:[[MPMediaItemArtwork alloc] initWithImage:newImage]
forKey:MPMediaItemPropertyArtwork];
//设置歌曲时长
[dict setObject:[NSNumber numberWithDouble:300] forKey:MPMediaItemPropertyPlaybackDuration];
//设置已经播放时长
[dict setObject:[NSNumber numberWithDouble:150] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//更新字典
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];

注:实现后台播放显示歌词的方法实现思路:将歌词信息,绘制到专辑图片上进行显示

后台播放远程控制

1.注册接受远程控制

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidAppear:(BOOL)animated {
// 接受远程控制
[self becomeFirstResponder];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

- (void)viewDidDisappear:(BOOL)animated {
// 取消远程控制
[self resignFirstResponder];
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
}

2.实现监听方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
if (event.type == UIEventTypeRemoteControl) {
//判断是否为远程控制
switch (event.subtype) {
//播放
case UIEventSubtypeRemoteControlPlay:
if (![self.audioPlayer isPlaying]) {
[self.audioPlayer play];
}
break;
case UIEventSubtypeRemoteControlPause:
//暂停
if ([self.audioPlayer isPlaying]) {
[self.audioPlayer pause];
}
break;
case UIEventSubtypeRemoteControlNextTrack:
NSLog(@"下一首");
break;
case UIEventSubtypeRemoteControlPreviousTrack:
NSLog(@"上一首 ");
break;
default:
break;
}
}
}

注:
event是事件类型对象,type是事件类型枚举

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, //触摸事件
UIEventTypeMotion, //运动事件
UIEventTypeRemoteControl, // 远程控制事件
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),//物理按钮事件类型
};

subtype中的枚举便是点击这些控制键后传递给我们的消息,我们可以根据这些消息在app内做逻辑处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,
// for UIEventTypeMotion, available in iPhone OS 3.0
//摇晃
UIEventSubtypeMotionShake = 1,
//UIEventTypeRemoteControl相关的枚举信息
// for UIEventTypeRemoteControl, available in iOS 4.0
//点击播放按钮或者耳机线控中间那个按钮
UIEventSubtypeRemoteControlPlay = 100,
//点击暂停按钮
UIEventSubtypeRemoteControlPause = 101,
//点击停止按钮
UIEventSubtypeRemoteControlStop = 102,
//点击播放与暂停开关按钮(iphone抽屉中使用这个)
UIEventSubtypeRemoteControlTogglePlayPause = 103,
//点击下一曲按钮或者耳机中间按钮两下
UIEventSubtypeRemoteControlNextTrack = 104,
//点击上一曲按钮或者耳机中间按钮三下
UIEventSubtypeRemoteControlPreviousTrack = 105,
//快退开始 点击耳机中间按钮三下不放开
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
//快退结束 耳机快退控制松开后
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
//开始快进 耳机中间按钮两下不放开
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
//快进结束 耳机快进操作松开后
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

拓展:后台播放远程控制的相关自定义

MPRemoteCommandCenter属于\<MediaPlayer/MediaPlayer.h>框架,是一个单例类,处理远程控制中心事件
可以自定义你的控制中心,锁屏时控制,siri语音控制.它管理一系列命令Playback Commands,Feedback Command,Previous/Next Track Commands),Skip Interval Commands,Seek Command等
MPRemoteCommand是这些所有命令的父类
下面简单介绍:

1
2
3
4
5
6
7
8
9
10
11
//是否开启命令
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;

//Target-action 添加事件处理
- (void)addTarget:(id)target action:(SEL)action;
//当属性enable = NO时,移除
- (void)removeTarget:(id)target action:(nullable SEL)action;
- (void)removeTarget:(nullable id)target;

//返回一个事件对象 MPRemoteCommandEvent是一个命令事件的父类,对应不同的命令进行了继承扩展,主要添加一些属性,比如拖拽音乐进度的positionTime
- (id)addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler;

远程控制三部曲

1:监听远程控制事件

1
- (void)beginReceivingRemoteControlEvents

2:捕获远程控制事件

1
- (void)remoteControlReceivedWithEvent:(UIEvent *)event

3.取消远程控制事件

1
- (void)endReceivingRemoteControlEvents

网易云的锁屏实现

注:添加的RemoteComand必须设置Enable为YES,并添加Target才会显示,并且添加相同类型RemoteComand,有优先级的选择显示,更改原有默认显示的上一曲,播放/暂停,下一曲会失效,需要重新添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
MPRemoteCommandCenter *rcc = [MPRemoteCommandCenter sharedCommandCenter];

//feedBack
[rcc.likeCommand setEnabled:YES];
//使用block
[rcc.likeCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
NSLog(@"like!");
return MPRemoteCommandHandlerStatusSuccess;
}];
rcc.likeCommand.localizedTitle = @"喜欢";


[rcc.dislikeCommand setEnabled:YES];
//使用target-action 不喜欢
[rcc.dislikeCommand addTarget:self action:@selector(previousCommandAction)];
rcc.dislikeCommand.localizedTitle = @"上一首";
[rcc.bookmarkCommand setEnabled:YES];
//取消Action
// [rcc.dislikeCommand removeTarget:self action:@selector(previousCommandAction)];


//书签
[rcc.bookmarkCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
NSLog(@"bookmark");
return MPRemoteCommandHandlerStatusSuccess;
}];
rcc.bookmarkCommand.localizedTitle = @"收藏";

效果图:
3

锁屏控制播放进度

主要使用:MPChangePlaybackPositionCommand

1
2
3
4
5
6
7
8
//改变音乐播放进度
MPChangePlaybackPositionCommand *changePlayBackPosition = [rcc changePlaybackPositionCommand];
[changePlayBackPosition setEnabled:YES];
[changePlayBackPosition addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
MPChangePlaybackPositionCommandEvent *events = (MPChangePlaybackPositionCommandEvent *)event;
self.audioPlayer.currentTime = events.positionTime;
return MPRemoteCommandHandlerStatusSuccess;
}];

流式音频播放(播放网络音频)

使用AudioToolbox框架中的音频队列服务Audio Queue Services。
第三方框架:FreeStreamer

#import “FSAudioStream.h”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化
FSAudioStream *player = [[FSAudioStream alloc]initWithUrl:[self getFileUrl]];
player.onFailure = ^(FSAudioStreamError error, NSString *errorDescription) {
NSLog(@"播放错误error:%@",errorDescription);
};
player.onCompletion = ^{
NSLog(@"播放完成");
};
//设置音量
[player setVolume:0.5];
//播放
[player play];

//添加引用
self.audioStream = player;

豆瓣:DOUAudioStreamer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "DOUAudioStreamer.h"


@interface AudioModel : NSObject<DOUAudioFile>

@property (nonatomic, strong) NSURL *audioFileURL;

@end

@implementation AudioModel

- (NSURL *)audioFileURL{
NSString *urlStr= @"http://sc1.111ttt.com/2017/1/03/07/296072048390.mp3";
NSURL *url=[NSURL URLWithString:urlStr];
self.audioFileURL = url;
return url;
}

@end

AudioModel *file = [[AudioModel alloc]init];
[file audioFileURL];
self.stream = [[DOUAudioStreamer alloc]initWithAudioFile:file];
[self.stream play];

参考:
https://developer.apple.com/library/content/samplecode/MPRemoteCommandSample/Introduction/Intro.html