iOS开发:本地音频播放

<h2>一、音频</h2>
在iOS中音频播放从形式上可以分为音效播放和音乐播放。前者主要指一些段音频播放,通常作为点缀音频,对于这类音频不需要进行进度、循环控制。后者指的是一些较长的音频,通常是主音频,对于这些音频,播放通常需要精确的控制。在iOS中播放两类视频分别使用AudioToolbox.framework 和 AVFoundation.framework 来完成音效和音乐播放。
<h3>1、音效</h3>
AudioToolbox.framework 是一套基于C语言的框架,使用它来播放音效其本质是将短音频注册到系统声音服务(System Sound Service)。System Sound Service 是一种简单、底层的声音播放服务,但它本身存在一些限制:

  • 音频播放时间不能够超过30s
  • 数据必须是PCM或者IMA4格式
  • 音频文件必须打包成.caf、.aif、.wav 中的一种(这是官方说法,实际测试中一些.mp3 也可以播放)

<b>使用System Sound Service 播放音效的步骤如下:</b>
I:调用<i><b>AudioServicesCreateSystemSoundID(</b>CFURLRef inFileURL, SystemSoundID *outSystemSoundID<b>)</b></i>函数获得系统声音ID。

II:如果需要监听播放完成操作,则使用<i><b>AudioServicesAddSystemSoundCompletion(</b>SystemSoundID inSystemSoundID, CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, void *inClientData<b>)</b></i>方法注册回调函数。
III:调用<i><b>AudioServicesPlaySystemSound(</b>SystemSoundID inSystemSoundID<b>)</b></i>或者<i><b>AudioServicesPlayAlertSound(</b>SystemSoundID inSystemSoundID<b>)</b></i> 方法播放音效(后者带有震动效果)。

下面是一个简单的示例程序:

<pre>

  • (void)playSoundEffect:(NSString *)name
    {
    NSString audioFile = [[NSBundle mainBundle]pathForResource:name ofType:nil];
    NSURL fileUrl = [NSURL fileURLWithPath:audioFile];
    // 获得系统声音ID
    SystemSoundID soundID = 0;
    /
    • inFileUrl:音频文件url
    • outSystemSoundID: 声音ID(此函数会将音效文件加入到系统音频服务中并返回一个ID)
      */
      AudioServicesCreateSystemSoundID((__bridge CFURLRef)fileUrl, &soundID);
      // 如果播放完需要进行某些操作,可以调用如下方法注册一个播放完的回调函数
      AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallback, NULL);
      // 播放音频
      AudioServicesPlaySystemSound(soundID);
      //AudioServicesPlayAlertSound(soundID); // 带震动
      }

pragma mark 播放完的回调函数 这是一个C函数

void soundCompleteCallback(SystemSoundID soundID, void *clientData) {
NSLog(@"播放完成。。。。");
}
</pre>

<h3>2、音乐</h3>

<h4>(1)概述</h4>

如果播放较大的音频或者要对音频有精确的控制System Sound Service 可能就很难满足实际需求了,通常这种情况会选择使用AVFoundation.framework中的AVAudioPlayer来实现。AVAudioPlayer可以看成一个播放器,它支持多种音频格式,而且能够进行进度、音量、播放速度等控制。首先简单看一下AVAudioPlayer常用的属性和方法:

<h5>属性</h5>

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

<h5>方法</h5>

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

<h5>代理方法</h5>

代理方法 说明
<i>- (void)<b>audioPlayerDidFinishPlaying:</b>(AVAudioPlayer *)player <b>successfully:</b>(BOOL)Flag</i> 音频播放完成
<i>- (void)<b>audioPlayerDecodeErrorDidOccur:</b>(AVAudioPlayer *)player <b>error:</b>(NSError *)error</i> 音频解码发生错误

<h4>(2)AVAudioPlayer的使用:</h4>

  • 初始化AVAudioPlayer对象,此时通常指定本地文件路径。
  • 设置播放属性,例如重复此时、音量大小等。
  • 调用play方法播放。

下面就是要AVAudioPlayer实现一个简单播放器,在这个播放器中实现了播放、暂停、显示播放进度、调节音频播放状态功能,当然例如调节音量、设置循环模式。甚至是声波图像等功能都可以实现,这里不再一一演示。界面效果如下:

当然由于AVAudioPlayer一次只能播放一个音频文件,所有上一曲、下一曲其实可以通过创建多个播放器对象来完成,这里暂不实现。如果下一曲可以播放的话,通常下一曲功能可以在代理方法中触发。为了不使其他关于铺设界面的代码干扰AVAudioPlayer的理解,下面只有核心代码:

#import "ViewController.h"
#import "PlayToolBar.h"
#import "Masonry.h"
#import "UIImage+Common.h"

#import <AVFoundation/AVFoundation.h>
@interface ViewController ()<AVAudioPlayerDelegate>
@property (nonatomic, strong) UILabel *songWordLabel;
@property (nonatomic, strong) UISlider *slider;
@property (nonatomic, strong) UIButton *playBtn;
@property (nonatomic, strong) UILabel *currentTimeLabel;
@property (nonatomic, strong) UILabel *maxTimeLabel;
@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
@property (nonatomic, assign) NSTimer *timer;
@property (nonatomic, strong) NSMutableArray *songWordArray;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self handleData];
    [self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
    self.navigationController.navigationBar.shadowImage = [UIImage new];
    [self createSubviews];
    [self layoutSubviewsByMasonry];
}
#pragma mark - button slider click
- (void)playAction:(UIButton *)sender
{
    if (sender.tag == 100) {
        [sender setBackgroundImage:[[UIImage imageNamed:@"mymusic_guess_like_pause"] imageWithColor:[UIColor colorWithRed:0.137 green:0.756 blue:0.49 alpha:1]] forState:UIControlStateNormal];
        [self play];
        sender.tag = 200;
    } else {
        [sender setBackgroundImage:[[UIImage imageNamed:@"play"] imageWithColor:[UIColor colorWithRed:0.137 green:0.756 blue:0.49 alpha:1]] forState:UIControlStateNormal];
        [self pause];
        sender.tag = 100;
    }
}
/** slider 的点击开始 */
- (void)sliderTouchBegin
{
    if ([self.audioPlayer isPlaying]) {
        _timer.fireDate = [NSDate distantFuture];
    }
}
/** slider 的点击结束 */
- (void)sliderSwipChangeValue
{
    if ([self.audioPlayer isPlaying]) {
        _audioPlayer.currentTime = _slider.value;
        _timer.fireDate = [NSDate distantPast];
    } else {
        [_slider setValue:0 animated:YES];
    }
}
#pragma mark - 定时器(懒加载)
- (NSTimer *)timer
{
    if (!_timer) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:YES];
    }
    return _timer;
}
- (void)updateProgress
{
    CGFloat progress = _audioPlayer.currentTime / _audioPlayer.duration;
    _currentTimeLabel.text = [self timeFormatted:self.audioPlayer.currentTime];
    [_slider setValue:progress * _slider.maximumValue animated:YES];
    NSInteger index = [_songWordArray indexOfObject:_currentTimeLabel.text];
    if (index >= 0 && index < _songWordArray.count) {
        _songWordLabel.text = _songWordArray[index + 1];
    }
}
#pragma mark - 播放器
/** audio�Player 创建(懒加载) */
- (AVAudioPlayer *)audioPlayer
{
    if (!_audioPlayer) {
        NSString *path = [[NSBundle mainBundle]pathForResource:@"周杰伦 - 稻香" ofType:@"mp3"];
        NSURL *url = [NSURL fileURLWithPath:path];
        _audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil];
        _audioPlayer.delegate = self;       
        _maxTimeLabel.text = [self timeFormatted:_audioPlayer.duration];
        _slider.maximumValue = _audioPlayer.duration;
        // 设置播放属性
        _audioPlayer.numberOfLoops = 0; // 不循环
        [_audioPlayer prepareToPlay]; // 准备播放,加载音频文件到缓存   
    }
    return _audioPlayer;
}
- (NSString *)timeFormatted:(float)timeInterval
{
    int time = (int)timeInterval;
    int seconds = time % 60;
    int minutes = (time / 60) % 60;
    int hours = time / 3600;
    if (hours == 0) {
        return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds];
    }
    return [NSString stringWithFormat:@"%02d:%02d:%02d", hours, minutes, seconds];
}
/** 播放 */
- (void)play
{
    if (![self.audioPlayer isPlaying]) {
        [self.audioPlayer play];
        self.timer.fireDate = [NSDate distantPast];//回复定时器
    }
}
- (void)pause
{
    if ([self.audioPlayer isPlaying]) {
        [self.audioPlayer pause];
        self.timer.fireDate = [NSDate distantFuture];
    }
}
/** audioPlayer delegate */
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
    [_playBtn setBackgroundImage:[[UIImage imageNamed:@"play"] imageWithColor:[UIColor colorWithRed:0.137 green:0.756 blue:0.49 alpha:1]] forState:UIControlStateNormal];
    [_timer finalize];
}

参照博文链接这里有更加详细的介绍!

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

推荐阅读更多精彩内容