iOS 利用音頻會話(audio session)實現可管理的音頻環境,音頻會話提供簡單實用的方法使 OS 得知應用程序應該如何與 iOS 音頻環境進行交互。AVFoundation 定義了 7 種分類來描述音頻行爲數組
分類 | 做用 | 是否容許混音 | 音頻輸入輸出模式 | 是否支持後臺 | 是否遵循靜音切換 |
---|---|---|---|---|---|
Ambient | 遊戲、效率應用程序 | 支持 | O | 不支持 | 不支持 |
Solo Ambient(default) | 遊戲、效率應用程序 | 不支持 | O | 不支持 | 遵循 |
Playback | 音頻和視頻播放器 | 可選 | O | 支持 | 不遵循 |
Record | 錄音機、音頻捕捉 | 不支持 | I | 支持 | 不遵循 |
Play and Record | VoIP、語音聊天 | 可選 | I/O | 支持 | 不遵循 |
Audio Processing | 離線會話和處理 | F | 不能播放和錄製 | 不遵循 | |
Multi-Route | 使用外部硬件的高級 A/V 應用程序 | F | I/O | 不遵循 |
同時能夠用 options 和 modes 進一步自定義開發。網絡
options 有如下選項session
支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute,AVAudioSessionCategoryAmbient 自動設置了此選項,AVAudioSessionCategoryOptionDuckOthers 和AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 也自動設置了此選項。若是使用這個選項激活會話,應用程序的音頻不會中斷從其餘應用程序(如音樂應用程序)的音頻,不然激活會話會打斷其餘音頻會話。less
支持 AVAudioSessionCategoryAmbient,AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute。設置此選項可以在播放音頻時低音量聽到後臺播放的其餘音頻。整個選項週期與會話激活週期一致。ide
支持 AVAudioSessionCategoryRecord,AVAudioSessionCategoryPlayAndRecord;容許藍牙免提設備啓用。當應用使用 setPreferredInput:error: 方法選擇了藍牙無線設備做爲輸入時,也會自動選擇相應的藍牙設備做爲輸出,使用 MPVolumeView 對象將藍牙設備做爲輸出時,輸入也會相應改變。oop
支持 AVAudioSessionCategoryPlayAndRecord;在沒有其餘的音頻路徑(如耳機)可使用的狀況下設置這個選項,會議音頻將經過設備的內置揚聲器播放。當不設置此選項,而且沒有其餘的音頻輸出可用或選擇時,音頻將經過接收器播放。只有 iPhone 設備都配備有一個接收器; iPad 和 iPod touch 設備,此選項沒有任何效果ui
當你的 iPhone 接有多個外接音頻設備時(耳塞,藍牙耳機等),AudioSession 將遵循 last-in wins 的原則來選擇外接設備,即聲音將被導向最後接入的設備。編碼
當沒有接入任何音頻設備時,通常狀況下聲音會默認從揚聲器出來,但有一個例外的狀況:在 PlayAndRecord 這個 category 下,聽筒會成爲默認的輸出設備。若是你想要改變這個行爲,能夠提供 MPVolumeView 來讓用戶切換到揚聲器,也可經過 overrideOutputAudioPort 方法來 programmingly 切換到揚聲器,也能夠修改 category option 爲AVAudioSessionCategoryOptionDefaultToSpeaker。url
支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute,設置此選項能使應用程序的音頻會話與其餘會話混合,可是會中斷使用了 AVAudioSessionModeSpokenAudio 模式的會話。其餘應用的音頻會在此會話啓動後暫停,並在此會話關閉後從新恢復。spa
在用到 AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 選項時,中斷了其餘應用的音頻後,本身的應用音頻結束播放時,若想恢復其餘應用的音頻,須要在關閉音頻會話的時候設置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 選項
[session setActive:NO
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:<#Your error object, or nil for testing#>];
複製代碼
支持 AVAudioSessionCategoryPlayAndRecord,容許會話在 AirPlay 設備上執行。
mode 用於定製化 audio sessions,若是將分類的 mode 設置不合理會執行默認的模式行爲,如將 AVAudioSessionCategoryMultiRoute 類別設置 AVAudioSessionModeGameChat 模式。
AVAudioSessionModeDefault 默認音頻會話模式
AVAudioSessionModeVoiceChat 若是應用須要執行例如 VoIP 類型的雙向語音通訊則選擇此模式
AVAudioSessionModeVideoChat 若是應用正在進行在線視頻會議,請指定此模式
AVAudioSessionModeGameChat 該模式由Game Kit 提供給使用 Game Kit 的語音聊天服務的應用程序設置
AVAudioSessionModeVideoRecording 若是應用正在錄製電影,則選此模式
AVAudioSessionModeMeasurement 若是您的應用正在執行音頻輸入或輸出的測量,請指定此模式
AVAudioSessionModeMoviePlayback 若是您的應用正在播放電影內容,請指定此模式
AVAudioSessionModeSpokenAudio 當須要持續播放語音,同時但願在其餘程序播放短語音時暫停播放此應用語音,選取此模式
首先得到指向 AVAudioSession 的單例指針,設置合適的分類,最後激活會話。
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
NSLog(@"Category Error: %@", [error localizedDescription]);
}
if (![session setActive:YES error:&error]) {
NSLog(@"Activation Error: %@", [error localizedDescription]);
}
複製代碼
AVAudioPlayer 構建於 Core Audio 的 C-based Audio Queue Services 最頂層,侷限性在於沒法從網絡流播放音頻,不能訪問原始音頻樣本,不能知足很是低的時延。
能夠經過 NSData 或本地音頻文件的 NSURL 兩種方式建立 AVAudioPlayer。
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"mp3"];
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
if (self.player) {
[self.player prepareToPlay];
}
複製代碼
建立出 AVAudioPlayer 後建議調用 prepareToPlay 方法,這個方法會取得須要的音頻硬件並預加載 Audio Queue 的緩衝區,固然若是不主動調用,執行 play 方法時也會默認調用,可是會形成輕微播放的延時。
AVAudioPlayer 的 play 能夠播放音頻,stop 和 pause 均可以暫停播放,可是 stop 會撤銷調用 prepareToPlay 所作的設置。
NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
for (AVAudioPlayer *player in self.players) {
[player playAtTime:delayTime];
}
self.playing = YES;
複製代碼
對於多個須要播放的音頻,若是但願同步播放效果,則須要捕捉當前設備時間並添加一個小延時,從而具備一個從開始播放時間計算的參照時間。deviveCurrentTime 是一個獨立於系統事件的音頻設備的時間值,當有多於 audioPlayer 處於 play 或者 pause 狀態時 deviveCurrentTime 會單調增長,沒有時置位爲 0。playAtTime 的參數 time 要求必須是基於 deviveCurrentTime 且大於等於 deviveCurrentTime 的時間。
for (AVAudioPlayer *player in self.players) {
[player stop];
player.currentTime = 0.0f;
}
複製代碼
暫停時須要將 audioPlayer 的 currentTime 值設置爲 0.0,當音頻正在播放時,這個值用於標識當前播放位置的偏移,不播放音頻時標識從新播放音頻的起始偏移。
player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;
複製代碼
若是但願應用程序播放音頻時屏蔽靜音切換動做,須要設置會話分類爲 AVAudioSessionCategoryPlayback,可是若是但願按下鎖屏後還能夠播放,就須要在 plist 里加入一個 Required background modes 類型的數組,在其中添加 App plays audio or streams audio/video using AirPlay。
中斷事件是指電話呼入、鬧鐘響起、彈出 FaceTime 等,中斷事件發生時系統會調用 AVAudioPlayer 的 AVAudioPlayerDelegate 類型的 delegate 的下列方法
- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 8_0);
- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0);
複製代碼
中斷結束調用的方法會帶入一個 options 參數,若是是 AVAudioSessionInterruptionOptionShouldResume 則代表能夠恢復播放音頻了。
在 iOS 設備上添加或移除音頻輸入、輸出線路時會引起線路改變,最佳實踐是,插入耳機時播放動做不改動,拔出耳機時應當暫停播放。
首先須要監聽通知
NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
[nsnc addObserver:self
selector:@selector(handleRouteChange:)
name:AVAudioSessionRouteChangeNotification
object:[AVAudioSession sharedInstance]];
複製代碼
而後判斷是舊設備不可達事件,進一步取出舊設備的描述,判斷舊設備是不是耳機,再作暫停播放處理。
- (void)handleRouteChange:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
AVAudioSessionRouteChangeReason reason =
[info[AVAudioSessionRouteChangeReasonKey] unsignedIntValue];
if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
AVAudioSessionRouteDescription *previousRoute =
info[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
NSString *portType = previousOutput.portType;
if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
[self stop];
[self.delegate playbackStopped];
}
}
}
複製代碼
這裏 AVAudioSessionPortHeadphones 只包含了有線耳機,無線藍牙耳機須要判斷 AVAudioSessionPortBluetoothA2DP 值。
AVAudioRecorder 用於負責錄製音頻。
建立 AVAudioRecorder 須要如下信息
NSString *tmpDir = NSTemporaryDirectory();
NSString *filePath = [tmpDir stringByAppendingPathComponent:@"memo.caf"];
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
NSDictionary *settings = @{
AVFormatIDKey : @(kAudioFormatAppleIMA4),
AVSampleRateKey : @44100.0f,
AVNumberOfChannelsKey : @1,
AVEncoderBitDepthHintKey : @16,
AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
};
NSError *error;
self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:&error];
if (self.recorder) {
self.recorder.delegate = self;
self.recorder.meteringEnabled = YES;
[self.recorder prepareToRecord];
} else {
NSLog(@"Error: %@", [error localizedDescription]);
}
複製代碼
prepareToRecord 方法執行底層 Audio Queue 初始化必要過程,並在指定位置建立文件。
AVFormatIDKey 鍵對應寫入內容的音頻格式,它有如下可選值
kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioFormatAppleLossless
kAudioFormatAppleIMA4
kAudioFormatiLBC
kAudioFormatULaw
複製代碼
kAudioFormatLinearPCM 會將未壓縮的音頻流寫入文件,文件體積大。kAudioFormatMPEG4AAC 和 kAudioFormatAppleIMA4 的壓縮格式會顯著縮小文件,並保證高質量音頻內容。可是要注意,制定的音頻格式與文件類型應該兼容,例如 wav 格式對應 kAudioFormatLinearPCM 值。
AVSampleRateKey 指示採樣率,即對輸入的模擬音頻信號每一秒內的採樣數。經常使用值 8000,16000,22050,44100。
AVNumberOfChannelsKey 指示定義記錄音頻內容的通道數,除非使用外部硬件錄製,不然一般選擇單聲道。
AVEncoderBitDepthHintKey 指示編碼位元深度,從 8 到 32。
AVEncoderAudioQualityKey 指示音頻質量,可選值有 AVAudioQualityMin, AVAudioQualityLow, AVAudioQualityMedium, AVAudioQualityHigh, AVAudioQualityMax。
錄音和播放應用應當使用 AVAudioSessionCategoryPlayAndRecord 分類來配置會話。
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
NSLog(@"Category Error: %@", [error localizedDescription]);
}
if (![session setActive:YES error:&error]) {
NSLog(@"Activation Error: %@", [error localizedDescription]);
}
複製代碼
注意錄音前須要申請麥克風權限。
對錄音過程的控制以下
[self.recorder record];
[self.recorder pause];
[self.recorder stop];
複製代碼
其中選擇了 stop 錄音即中止,此時 AVAudioRecorder 會調用其遵循 AVAudioRecorderDelegate 協議的代理的 - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
方法。
在初始化 AVAudioRecorder 時指定了臨時文件目錄做爲存儲音頻的位置,音頻錄製結束時須要保存到 Document 目錄下
NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate];
NSString *filename = [NSString stringWithFormat:@"%@-%f.m4a", name, timestamp];
NSString *docsDir = [self documentsDirectory];
NSString *destPath = [docsDir stringByAppendingPathComponent:filename];
NSURL *srcURL = self.recorder.url;
NSURL *destURL = [NSURL fileURLWithPath:destPath];
NSError *error;
BOOL success = [[NSFileManager defaultManager] copyItemAtURL:srcURL toURL:destURL error:&error];
if (success) {
handler(YES, [THMemo memoWithTitle:name url:destURL]);
[self.recorder prepareToRecord];
} else {
handler(NO, error);
}
複製代碼
這裏調用了 NSFileManager 的 copyItemAtURL 方法將文件內容拷貝到 Document 目錄下。
記錄音頻時須要展現時間提示用戶當前錄製時間,AVAudioRecorder 的 currentTime 屬性能夠獲知當前時間,將其格式化後便可進行展現
- (NSString *)formattedCurrentTime {
NSUInteger time = (NSUInteger)self.recorder.currentTime;
NSInteger hours = (time / 3600);
NSInteger minutes = (time / 60) % 60;
NSInteger seconds = time % 60;
NSString *format = @"%02i:%02i:%02i";
return [NSString stringWithFormat:format, hours, minutes, seconds];
}
複製代碼
可是須要實時展現時間的話,不能經過 KVO 來解決,只能加入到 NSTimer 中,每 0.5s 執行一次。
[self.timer invalidate];
self.timer = [NSTimer timerWithTimeInterval:0.5
target:self
selector:@selector(updateTimeDisplay)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
複製代碼
AVAudioRecorder 和 AVAudioPlayer 都有兩個方法獲取當前音頻的平均分貝和峯值分貝數據。
- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */
複製代碼
返回值從 -160dB(靜音) 到 0dB(最大分貝)。
獲取值以前要在初始化播放器或記錄器時設置 meteringEnabled 爲 YES。
首先須要將 -160 到 0 的分貝值轉爲 0 到 1 範圍內,須要用到下面這個類
@implementation THMeterTable {
float _scaleFactor;
NSMutableArray *_meterTable;
}
- (id)init {
self = [super init];
if (self) {
float dbResolution = MIN_DB / (TABLE_SIZE - 1);
_meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
_scaleFactor = 1.0f / dbResolution;
float minAmp = dbToAmp(MIN_DB);
float ampRange = 1.0 - minAmp;
float invAmpRange = 1.0 / ampRange;
for (int i = 0; i < TABLE_SIZE; i++) {
float decibels = i * dbResolution;
float amp = dbToAmp(decibels);
float adjAmp = (amp - minAmp) * invAmpRange;
_meterTable[i] = @(adjAmp);
}
}
return self;
}
float dbToAmp(float dB) {
return powf(10.0f, 0.05f * dB);
}
- (float)valueForPower:(float)power {
if (power < MIN_DB) {
return 0.0f;
} else if (power >= 0.0f) {
return 1.0f;
} else {
int index = (int) (power * _scaleFactor);
return [_meterTable[index] floatValue];
}
}
@end
複製代碼
接下來能夠實時獲取到分貝平均值和峯值
- (THLevelPair *)levels {
[self.recorder updateMeters];
float avgPower = [self.recorder averagePowerForChannel:0];
float peakPower = [self.recorder peakPowerForChannel:0];
float linearLevel = [self.meterTable valueForPower:avgPower];
float linearPeak = [self.meterTable valueForPower:peakPower];
return [THLevelPair levelsWithLevel:linearLevel peakLevel:linearPeak];
}
複製代碼
能夠看到獲取峯值和均值前必須調用 updateMeters 方法。