開篇
最近在學(xué)習(xí)AV Foundation
試圖把學(xué)習(xí)內(nèi)容記錄下來 并參考一些博客文章
本期的內(nèi)容是AVAudioPlayer
音頻知識基礎(chǔ)
音頻文件的生成過程是將聲音信息采樣骨宠、量化和編碼產(chǎn)生的數(shù)字信號的過程擦剑,人耳所能聽到的聲音凹蜂,最低的頻率是從20Hz起一直到最高頻率20KHZ民镜,因此音頻文件格式的最大帶寬是20KHZ捕捂。根據(jù)
奈奎斯特
的理論,只有采樣頻率高于聲音信號最高頻率的兩倍時替饿,才能把數(shù)字信號表示的聲音還原成為原來的聲音滤馍,所以音頻文件的采樣率一般在40~50KHZ,比如最常見的CD音質(zhì)采樣率44.1KHZ掸掸。 (所以一般大家都覺得CD音質(zhì)是最好的.) 對聲音進(jìn)行采樣氯庆、量化過程被稱為脈沖編碼調(diào)制
(Pulse Code Modulation),簡稱PCM扰付。PCM數(shù)據(jù)是最原始的音頻數(shù)據(jù)完全無損,所以PCM數(shù)據(jù)雖然音質(zhì)優(yōu)秀但體積龐大仁讨,為了解決這個問題先后誕生了一系列的音頻格式羽莺,這些音頻格式運(yùn)用不同的方法對音頻數(shù)據(jù)進(jìn)行壓縮,其中有無損壓縮(ALAC洞豁、APE盐固、FLAC)和有損壓縮(MP3、AAC丈挟、OGG刁卜、WMA)兩種 來源:iOS音頻播放 (一):概述 by 碼農(nóng)人生
–
我覺得程寅大牛的處理音頻說的很明白
大神列出一個經(jīng)典的音頻播放流程(以MP3為例)
- 讀取MP3文件
- 解析采樣率、碼率曙咽、時長等信息蛔趴,分離MP3中的音頻幀
- 對分離出來的音頻幀解碼得到PCM數(shù)據(jù)
- 對PCM數(shù)據(jù)進(jìn)行音效處理(均衡器、混響器等例朱,非必須)
- 把PCM數(shù)據(jù)解碼成音頻信號
- 把音頻信號交給硬件播放
- 重復(fù)1-6步直到播放完成
在iOS系統(tǒng)中apple對上述的流程進(jìn)行了封裝并提供了不同層次的接口
這是CoreAudio的接口層次
下面對其中的中高層接口進(jìn)行功能說明:
- Audio File Services:讀寫音頻數(shù)據(jù)孝情,可以完成播放流程中的第2步鱼蝉;
- Audio File Stream Services:對音頻進(jìn)行解碼,可以完成播放流程中的第2步箫荡;
- Audio Converter services:音頻數(shù)據(jù)轉(zhuǎn)換魁亦,可以完成播放流程中的第3步;
- Audio Processing Graph Services:音效處理模塊羔挡,可以完成播放流程中的第4步洁奈;
- Audio Unit Services:播放音頻數(shù)據(jù):可以完成播放流程中的第5步、第6步绞灼;
- Extended Audio File Services:Audio File Services和Audio
- Converter services的結(jié)合體利术;
- AVAudioPlayer/AVPlayer(AVFoundation):高級接口,可以完成整個音頻播放的過程(包括本地文件和網(wǎng)絡(luò)流播放镀赌,第4步除外)氯哮;
- Audio Queue Services:高級接口,可以進(jìn)行錄音和播放商佛,可以完成播放流程中的第3喉钢、5、6步良姆;
- OpenAL:用于游戲音頻播放肠虽,暫不討論
可以看到apple提供的接口類型非常豐富,可以滿足各種類別類需求:
如果你只是想實現(xiàn)音頻的播放玛追,沒有其他需求AVFoundation會很好的滿足你的需求税课。它的接口使用簡單、不用關(guān)心其中的細(xì)節(jié)痊剖;
如果你的app需要對音頻進(jìn)行流播放并且同時存儲韩玩,那么AudioFileStreamer加AudioQueue能夠幫到你,你可以先把音頻數(shù)據(jù)下載到本地陆馁,一邊下載一邊用NSFileHandler等接口讀取本地音頻文件并交給AudioFileStreamer或者AudioFile解析分離音頻幀找颓,分離出來的音頻幀可以送給AudioQueue進(jìn)行解碼和播放。如果是本地文件直接讀取文件解析即可叮贩。(這兩個都是比較直接的做法击狮,這類需求也可以用AVFoundation+本地server的方式實現(xiàn),AVAudioPlayer會把請求發(fā)送給本地server益老,由本地server轉(zhuǎn)發(fā)出去彪蓬,獲取數(shù)據(jù)后在本地server中存儲并轉(zhuǎn)送給AVAudioPlayer。另一個比較trick的做法是先把音頻下載到文件中捺萌,在下載到一定量的數(shù)據(jù)后把文件路徑給AVAudioPlayer播放档冬,當(dāng)然這種做法在音頻seek后就回有問題了。)
如果你正在開發(fā)一個專業(yè)的音樂播放軟件,需要對音頻施加音效(均衡器捣郊、混響器)辽狈,那么除了數(shù)據(jù)的讀取和解析以外還需要用到AudioConverter來把音頻數(shù)據(jù)轉(zhuǎn)換成PCM數(shù)據(jù),再由AudioUnit+AUGraph來進(jìn)行音效處理和播放(但目前多數(shù)帶音效的app都是自己開發(fā)音效模塊來坐PCM數(shù)據(jù)的處理呛牲,這部分功能自行開發(fā)在自定義性和擴(kuò)展性上會比較強(qiáng)一些刮萌。PCM數(shù)據(jù)通過音效器處理完成后就可以使用AudioUnit播放了,當(dāng)然AudioQueue也支持直接使對PCM數(shù)據(jù)進(jìn)行播放娘扩。)着茸。下圖描述的就是使用AudioFile + AudioConverter + AudioUnit進(jìn)行音頻播放的流程
以上內(nèi)容均轉(zhuǎn)自碼農(nóng)人生 希望大神不要介意 如果有問題 我可立即清除
使用AVAudioPlayer
之前對AudioSession簡介
AVAudioSession
負(fù)責(zé)管理音頻會話 它是個單例 在應(yīng)用程序和操作系統(tǒng)之間負(fù)責(zé)中間人的角色 AudioSession參考
AVAudioSession
主要功能包括以下幾點:
- app是如何使用的音頻服務(wù) 播放 還是錄制 之類的
- 控制協(xié)調(diào)app輸入輸出設(shè)備(比如 麥克風(fēng),耳機(jī)琐旁、手機(jī)外放比如藍(lán)牙連接一個外置音響 或airplay)
- 協(xié)調(diào)你的app的音頻播放和系統(tǒng)以及其他app行為(例如有電話時需要打斷涮阔,電話結(jié)束時需要恢復(fù),按下靜音按鈕時是否歌曲也要靜音等)
注:AVAudioSession iOS6以后使用 以前叫AudioSession
如何使用AVAudioPlayer
在我的博客里面我盡量使用code勝過千言萬語
使用AVAudioPlayer
之前需要在AppDelegate
里面導(dǎo)入#import <AVFoundation/AVFoundation.h>
并且啟動音頻會話
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) {
NSLog(@"Category Error: %@", [error localizedDescription]);
}
if (![session setActive:YES error:&error]) {
NSLog(@"Activation Error: %@", [error localizedDescription]);
}
return YES;
}
上邊已經(jīng)介紹了AVAudioSession
這里面說一下[session setCategory:AVAudioSessionCategoryPlayback error:&error]
里面的AVAudioSessionCategoryPlayback
這是這幾種分類的列表大家可以看下
記得開啟后臺播放
或者在plist里面修改
下面就是創(chuàng)建音頻播放器代碼
#import "ViewController.h"
#import <Masonry/Masonry.h>
#import "THControlKnob.h"
#import "THPlayButton.h"
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
//三個控制推子
@property (weak, nonatomic) IBOutlet THOrangeControlKnob *panKnob;
@property (weak, nonatomic) IBOutlet THOrangeControlKnob *volumnKnob;
@property (weak, nonatomic) IBOutlet THGreenControlKnob *rateKnob;
@property (weak, nonatomic) IBOutlet THPlayButton *playButton;
//音樂播放器
@property (nonatomic, strong) AVAudioPlayer *musicPlayer;
@property (nonatomic, getter = isPlaying) BOOL playing; //播放狀態(tài)
//無關(guān)代碼
@property (weak, nonatomic) IBOutlet UILabel *LeftRightRoundDec;
@property (weak, nonatomic) IBOutlet UILabel *voiceDec;
@property (weak, nonatomic) IBOutlet UILabel *rateDec;
@property (weak, nonatomic) IBOutlet UILabel *trackDescrption;
@end
導(dǎo)入幾個第三方控件的類用于音樂播放
這上邊的三個旋鈕就是導(dǎo)入的開源庫
下面創(chuàng)建播放器AVAudioPlayer
創(chuàng)建時需要一個NSURL
代表要播放的文件路徑 這里簡單從bundle中拖了一首歌進(jìn)去了
#pragma mark -
#pragma mark - 創(chuàng)建AVAudioPlayer與播放狀態(tài)控制
/**
創(chuàng)建音樂播放器
@param fileName 文件名
@param fileExtension 文件擴(kuò)展名
@return 播放器實例
*/
- (AVAudioPlayer *)createPlayForFile:(NSString *)fileName
withExtension:(NSString *)fileExtension{
NSURL *url = [[NSBundle mainBundle] URLForResource:fileName withExtension:fileExtension];
NSError *error = nil;
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
if (audioPlayer) {
audioPlayer.numberOfLoops = -1; //-1無限循環(huán)
audioPlayer.enableRate = YES; //啟動倍速控制
[audioPlayer prepareToPlay];
} else {
NSLog(@"Error creating player: %@",[error localizedDescription]);
}
return audioPlayer;
}
numberOfLoops
= -1; 代表本首歌 無限循環(huán) 其它常數(shù)代表循環(huán)次數(shù)
enableRate
代表是否啟用倍速調(diào)節(jié) 0.5x 1.0x 2.0x 等倍速 1.0代表正常速度
這里說一下[audioPlayer prepareToPlay]
調(diào)用這個函數(shù)是為了取得需要的音頻硬件并預(yù)加載Audio Queue
的緩沖區(qū). 當(dāng)然也可以不調(diào)用這個方法直接調(diào)用 [audioPlayer play]
灰殴,但當(dāng) 調(diào)用play
方法時也會隱性激活,調(diào)用prepareToPlay
是為了減少 創(chuàng)建播放器時預(yù)設(shè)加載和聽到聲音輸出之間的延時
@implementation ViewController
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
if (self.musicPlayer == nil) {
self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"];
}
[self setupNotifications];
}
return self;
}
- (void)awakeFromNib{
[super awakeFromNib];
if (self.musicPlayer == nil) {
self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"];
}
[self setupNotifications];
}
在
initWithNibName
或awakeFromNib
時候調(diào)用一下創(chuàng)建播放器的代碼
這個[self setupNotifications];
后面說
先添加一些常見的方法封裝 比如 播放敬特、暫停、停止
- (void)play {
if (self.musicPlayer == nil) { return; }
if (!self.playing) {
NSTimeInterval delayTime = [self.musicPlayer deviceCurrentTime] + 0.01;
[self.musicPlayer playAtTime:delayTime];
self.playing = YES;
}
self.trackDescrption.text = [self.musicPlayer.url absoluteString];
[self configNowPlayingInfoCenter]; //配置后臺播放的頁面信息
}
- (void)stop {
if (self.musicPlayer == nil) { return; }
if (self.playing) {
[self.musicPlayer stop];
self.musicPlayer.currentTime = 0.0f;
self.playing = NO;
}
}
- (void)pause {
if (self.musicPlayer == nil) { return; }
if (self.playing) {
[self.musicPlayer pause];
self.playing = NO;
}
}
這里看到[self.musicPlayer deviceCurrentTime] + 0.01
加了 -0.01的延時, 是為了以后大家做播放器的時候 有可能暫臀眨或者歌曲切換時 有可能 向前向后做片段銜接, 也是為了使用 playAtTime
去播放 指定位置的音樂用于 意外暫臀袄或者播放上次播放的配置信息使用 這里看到我寫了一個
[self configNowPlayingInfoCenter];
配置后臺播放的頁面信息
這個主要用于播放音樂在后臺時 鎖屏顯示的屏幕信息 請看下面代碼
//設(shè)置鎖屏狀態(tài),顯示的歌曲信息
-(void)configNowPlayingInfoCenter{
if (NSClassFromString(@"MPNowPlayingInfoCenter")) {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
//歌曲名稱
[dict setObject:@"歌曲名稱" forKey:MPMediaItemPropertyTitle];
//演唱者
[dict setObject:@"演唱者" forKey:MPMediaItemPropertyArtist];
//專輯名
[dict setObject:@"專輯名" forKey:MPMediaItemPropertyAlbumTitle];
//專輯縮略圖
UIImage *image = [UIImage imageNamed:@"sunyazhou"];
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image];
[dict setObject:artwork forKey:MPMediaItemPropertyArtwork];
//音樂剩余時長
[dict setObject:@20 forKey:MPMediaItemPropertyPlaybackDuration];
//音樂當(dāng)前播放時間 在計時器中修改
// [dict setObject:[NSNumber numberWithDouble:100.0] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//設(shè)置鎖屏狀態(tài)下屏幕顯示播放音樂信息
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
}
如果需要在計時器中不斷刷新鎖屏狀態(tài)下的播放進(jìn)度條請寫如下代碼
//計時器修改進(jìn)度
- (void)changeProgress:(NSTimer *)sender{
if(self.player){
//當(dāng)前播放時間
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
[dict setObject:[NSNumber numberWithDouble:self.player.currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //音樂當(dāng)前已經(jīng)過時間
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
}
下面我們來介紹一下
[self setupNotifications];
注冊監(jiān)聽 音頻意外中斷和耳機(jī)拔出時要暫停音樂播放
實現(xiàn)代碼如下
/**
播放的通知處理
*/
- (void)setupNotifications {
NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
//添加意外中斷音頻播放的通知
[nsnc addObserver:self
selector:@selector(handleInterruption:)
name:AVAudioSessionInterruptionNotification
object:[AVAudioSession sharedInstance]];
//添加線路變化通知
[nsnc addObserver:self
selector:@selector(hanldeRouteChange:)
name:AVAudioSessionRouteChangeNotification
object:[AVAudioSession sharedInstance]];
}
注:記得在delloc里面[[NSNotificationCenter defaultCenter] removeObserver:self]
意外中斷音頻發(fā)生的場景 例如 聽歌過程中來電話或者 按住home鍵使用siri
下面是具體方法實現(xiàn)
/**
音頻意外打斷處理
@param notification 通知信息
*/
- (void)handleInterruption:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (type == AVAudioSessionInterruptionTypeBegan) {
//Handle AVAudioSessionInterruptionTypeBegan
[self pause];
} else {
//Handle AVAudioSessionInterruptionTypeEnded
AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
NSError *error = nil;
//激活音頻會話 允許外接音響
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
if (options == AVAudioSessionInterruptionOptionShouldResume) {
[self play];
} else {
[self play];
}
self.playButton.selected = YES;
if (error) {
NSLog(@"AVAudioSessionInterruptionOptionShouldResume失敗:%@",[error localizedDescription]);
}
}
}
先說handleInterruption
意外情況下中斷比如我按住home鍵使用siri
我會收到意外打斷的通知當(dāng) type == AVAudioSessionInterruptionTypeBegan
時 我們停止音樂播放或者暫停.
當(dāng)type != AVAudioSessionInterruptionTypeBegan
的時候一定是AVAudioSessionInterruptionTypeEnded
這個時候notification.userInfo
里面包含一個AVAudioSessionInterruptionOptions
值來表明音頻會話是否已經(jīng)重新激活以及是否可以再次播放
注:這個地方遇到個坑 當(dāng)意外中斷時候有時音頻會話會很不靈敏 后來發(fā)現(xiàn)這種情況下需要重新激活會話 如下代碼:
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
這里AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
是為了通知其它應(yīng)用會話被我激活了 很多播放器開發(fā)者很不講究 每次從來不用這個方法導(dǎo)致每次別人播放完音頻 自己都收不到音頻重新播放的信息 建議大家以和為貴, 寫良心代碼.
因為我外接的小米藍(lán)牙音響發(fā)現(xiàn)還是不好使 最后又補(bǔ)上了AVAudioSessionCategoryOptionAllowBluetooth
這個
激活音頻會話 允許外接音響
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];
就好使了
下面說一下耳機(jī)插拔或者USB麥克風(fēng)斷開 Apple有個什么Human Interface Guidelines(HIG)
相關(guān)定義 意思是說當(dāng)硬件耳機(jī)拔出時建議 暫停播放音樂或者麥克風(fēng)斷開時掰伸。就是處于靜音狀態(tài)皱炉。是為了保密播放內(nèi)容不被外界聽到,不管蘋果啥規(guī)定 我們都得照辦 否則就得被拒。
- (void)hanldeRouteChange:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
AVAudioSessionRouteChangeReason reason = [info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
//老設(shè)備不可用
if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
AVAudioSessionRouteDescription *previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
NSString *portType = previousOutput.portType;
if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
[self stop];
self.playButton.selected = NO;
}
}
}
這需要用AVAudioSessionRouteChangeReasonKey
取出線路切換的原因AVAudioSessionRouteChangeReason
原因有這么多
typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason)
{
AVAudioSessionRouteChangeReasonUnknown = 0,
AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
AVAudioSessionRouteChangeReasonCategoryChange = 3,
AVAudioSessionRouteChangeReasonOverride = 4,
AVAudioSessionRouteChangeReasonWakeFromSleep = 6,
AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8
} NS_AVAILABLE_IOS(6_0);
我們需要這個AVAudioSessionRouteChangeReasonOldDeviceUnavailable
判斷是否是舊設(shè)備
通過AVAudioSessionRouteChangePreviousRouteKey
拿出
AVAudioSessionRouteDescription
描述信息
previousRoute
在通過
previousRoute.outputs[0]
拿出AVAudioSessionPortDescription
拿出NSString *portType = previousOutput.portType
如果[portType isEqualToString:AVAudioSessionPortHeadphones]
如果是耳機(jī)AVAudioSessionPortHeadphones
則暫停播放
以上就是中斷和線路切換的一些代碼邏輯
下面我介紹一些好玩的
前面說的一些后臺設(shè)置信息顯示的內(nèi)容就是上圖所示 在鎖屏的時候顯示
但是大家一定很奇怪的是怎么實現(xiàn)接收 鎖屏狀態(tài)下 點擊 上一曲 暫停/播放 下一曲等操作
需要在AppDelegate里面寫上
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) {
NSLog(@"Category Error: %@", [error localizedDescription]);
}
if (![session setActive:YES error:&error]) {
NSLog(@"Activation Error: %@", [error localizedDescription]);
}
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
return YES;
}
這[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
行代碼 以及調(diào)用自己為 [self becomeFirstResponder];
第一響應(yīng)者 這樣寫是為了應(yīng)用響應(yīng)音頻播放 后臺切換或者中斷的時候更靈敏.
- (BOOL)canBecomeFirstResponder {
return YES;
}
然后 寫上如下代碼 處理鎖屏狀態(tài)下 點擊 上一曲 暫停/播放 下一曲等操作
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
if (event.type == UIEventTypeRemoteControl) {
switch (event.subtype) {
case UIEventSubtypeRemoteControlPlay:
NSLog(@"暫停播放");
break;
case UIEventSubtypeRemoteControlPause:
NSLog(@"繼續(xù)播放");
break;
case UIEventSubtypeRemoteControlNextTrack:
NSLog(@"下一曲");
break;
case UIEventSubtypeRemoteControlPreviousTrack:
NSLog(@"上一曲");
break;
default:
break;
}
}
}
剩余邏輯大家自己填充吧我就不介紹了.
好了AVAudioPlayer就到這吧狮鸭!有啥疑問大家可以評論留言都能看到或者指正我的錯誤合搅。我會及時改正.
全文完
文章最終的Demo獲取:加iOS高級技術(shù)交流群:
624212887
歧蕉,獲取Demo,以及更多iOS學(xué)習(xí)資料
文章來源于網(wǎng)絡(luò)灾部,如有侵權(quán)請聯(lián)系小編刪除