音頻錄制和播放的詳細分析及實現
一、前言
-
這段時間確實忙,好久沒更新文章了,實在抱歉!中間換了工作環境,暫時穩定下來了,抽空整理了之前看的資料,具體分析了音頻錄制以及播放的實現,并封裝了一個錄音和播放音頻的 小輪子。
-
至于為什么寫這個,因為我確實是對音視頻方面比較感興趣!而且之前也在看這部分的文檔,我目的是要逐步研究音視頻底層的東西,而 AudioToolbox 、 VideoToolbox 就是底層實現了,在此之前,我覺得熟悉上層接口是很有必要的
圖片來源于官方文檔
- 那么本文研究的都是 AVFoundation 、 AVKit 、 MediaPlayer 這些上層接口類
二、上層接口以及基本用法
注意:此處只針對錄音和播放的上層相關核心類,稍作解釋,整理了兩張圖說明,圖片尺寸有點大,建議新標簽打開或者另存為打開。
核心類
核心類的簡單介紹
錄音
-
1、 AVAudioRecorder 文檔詳細介紹了相關API以及使用,通過這個類,可簡單實現音頻錄制、暫停、停止、指定時間錄制等,缺點是:沒辦法設置錄音時長,無法監聽錄音各種狀態;當然,本框架中已經實現,可仔細閱讀源碼
-
2、 Audio Queue Services 文檔描述也是相當詳細了,暫時沒仔細研究,本框架中暫時沒使用
播放
-
1、 System Sound Services 提供C接口實現,可播放本地bundle的短暫音效(30秒以內)和震動效果,會馬上播放,不支持多個同時播放,可通過 OSStatus 判斷是否操作成功
摘自文檔:
System Sound Services provides a C interface for playing short sounds and for invoking vibration on iOS devices that support vibration.You can use System Sound Services to play short (30 seconds or shorter) sounds.
用法如下:
// kSystemSoundID_Vibrate 震動 iPod 無效 // kSystemSoundID_UserPreferredAlert 播放用戶在系統設置的音效 SystemSoundID soundID; // 必須是bundle文件 NSURL *soundUrl = [[NSBundle mainBundle] URLForResource:@"sound" withExtension:@"wav"]; // 創建 AudioServicesCreateSystemSoundID((__bridge CFURLRef)soundUrl, &soundID); // 播放 //AudioServicesPlayAlertSound(soundID);// 提醒音效 AudioServicesPlayAlertSoundWithCompletion(soundID, ^{
}); //AudioServicesPlaySystemSound(soundID);// 普通系統聲音 //AudioServicesPlaySystemSoundWithCompletion(soundID, ^{
//});
// 停止 //AudioServicesDisposeSystemSoundID(soundID);
// 播放完畢回調 AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, (void *)finishPlayCallback, NULL);</code></pre>
/** 回調
@param soundID ID / static void finishPlayCallback(SystemSoundID soundID){ NSLog(@"stop"); }</code></pre> </li> </ul>
-
2、 AVAudioPlayer ,可實現播放本地音頻文件或緩存data音頻數據、延時播放、循環播放、可同時播放多個音頻、可控制播放優先級、播放速度等;支持播放iOS或macOS所支持的所有音頻格式,具體API官方文檔有詳細介紹
用法如下:
NSURL
// 準備播放、開始、暫停、結束 [player prepareToPlay]; [player play]; [player pause]; [player stop];</code></pre> </li> </ul>
-
3、 MPMusicPlayerController ,在 MediaPlayer 框架中,需要配合 MPMediaPickerController 使用,支持列表播放,由于系統封裝好,因此定制性不強,MPMusicPlayerController有兩種播放器:applicationMusicPlayer和systemMusicPlayer,前者在應用退出后音樂播放會自動停止,后者在應用停止后不會退出播放狀態。 具體用法可參考官方文檔
4、 Audio Queue Services ,支持網絡流媒體播放、支持常用格式、支持列表播放、定制性十分強,因為功能都需要自行實現,作者暫時沒在此框架中使用,其實在此之前,本人已經翻譯了 Audio Queue Services 的 Playing Audio ,推薦先看看 Audio Queue Services 解讀之 Playing Audio(上) 、 Audio Queue Services 解讀之 Playing Audio(下) ,大家可對比官方文檔查閱
-
5、 MPMoviePlayerController ,注意:這個類在iOS9.0之后就被廢棄了,系統建議使用 AVPictureInPictureController 或 AVPlayerViewController 代替,系統已經高度封裝了,因此定制性不強,但支持播放音頻,因此也可以使用此類來進行音頻播放,至于view就可以不顯示出來
摘自官方文檔:
The MPMoviePlayerController class is formally deprecated in iOS 9. (The MPMoviePlayerViewController class is also formally deprecated.) To play video content in iOS 9 and later, instead use the AVPictureInPictureController or AVPlayerViewController class from the AVKit framework, or the WKWebView class from WebKit.
用法如下:( 代碼摘自官方文檔 )
MPMoviePlayerController *player = [[MPMoviePlayerController alloc] initWithContentURL: myURL]; [player prepareToPlay]; [player.view setFrame: myView.bounds]; // player's frame must match parent's [myView addSubview: player.view]; // ... [player play];
6、 MPMoviePlayerViewController ,和 MPMoviePlayerController 一樣,iOS9.0之后就被廢棄了,具體用法和 MPMoviePlayerController 類似,這里不作過多分析,用到的朋友可先查閱官方文檔。
-
7、 AVPictureInPictureController ,看命名就知道,這個類就是專門處理畫中畫的,目前暫時只支持iPad,在此就不作過多分析,有需求的朋友自行查閱官方文檔。
摘自官方文檔:
An AVPictureInPictureController lets you respond to user-initiated playback of video in a floating, resizable window on iPad.
- 8、 AVPlayerViewController ,系統針對 AVPlayer 封裝的一個視頻播放器,當然支持播放音頻,支持 .mov、.mp4、.mpv、.3gp 格式,其他格式沒一一考究,按理來說, AVPlayerViewController 是對 AVPlayer 進行封裝的,而 AVPlayer 支持多種格式,那么 AVPlayerViewController 也應該支持,具體就交給各位去檢驗一下,有問題@我一下喔,此類具體用法在此也不作過多分析
-
9、 AVPlayer ,通過上表可以知道, AVPlayer 功能確實已經足夠強大,iOS4.0以上都支持,支持網絡流媒體、支持多種常用類型,可在 AVMediaFormat.h 通過 類 AVURLAsset 調用 audiovisualTypes 方法返回支持類型; stack over flow 有提供答案,可參考。
下面介紹一下相關類:
-
AVAsset :是一個抽象類,不能直接使用,主要用于準確獲取多媒體信息,例如媒體總時長duration、音量、播放速度等。
-
AVURLAsset : AVAsset 的子類,可根據一個本地或網絡URL路徑創建一個包含多媒體信息的 AVURLAsset 對象
-
AVAssetTrack :傳輸軌道,一般播放視頻至少兩個軌道,一個播放聲音,一個播放畫面;而 AVAssetTrack 就是專門管理這些軌道的,可以查看到媒體類型、軌道ID、采樣數據的長度等
-
AVPlayerItem :媒體資源管理對象,管理視頻或音頻的一些基本信息和狀態,通過KVO方便監聽播放狀態、緩沖進度等信息,一個 AVPlayerItem 對應一個視頻或音頻,可通過 NSURL 或 AVAsset 創建
-
AVQueuePlayer : AVPlayer 的子類,通過此類可以方便實現多媒體列表播放、可切換下一條播放
三、需求是什么
-
音頻錄制
由于不需要實時錄制傳輸,只要錄制到指定本地文件,保證格式可用,而且還沒仔細研究 Audio Queue Services ,因此暫時使用 AVAudioRecorder ,需要提供什么API?
-
1、一些基本配置參數,例如通道數、采樣率、音頻格式等
-
2、錄音狀態,正在錄音、暫停錄音、停止錄音
-
3、錄音結束時間,設置錄音在某一時間停止
-
4、音頻控制接口,準備播放(參數配置后)、開始播放、暫停播放、停止播放
-
5、音頻播放狀態監聽,通過代理監聽:錄音開始播放、正在錄音、結束錄音、錄音出現錯誤
-
-
音頻播放
涉及播放必須支持本地和網絡媒體,支持常用音頻格式、支持列表播放,而且可定制性必須強,通過上文表格可以發現,只有 Audio Queue Services 和 AVPlayer 滿足,當然,由于沒有仔細研究 Audio Queue Services ,因此暫時使用 AVPlayer ,那么需要什么API呢?
-
1、根據URL創建播放器,可傳入單個URL字符串或者URL字符串數組
-
2、動態添加新的URL地址,可傳入單個URL字符串或者URL字符串數組
-
3、播放器當前狀態,正在播放、暫停播放、停止播放
-
4、當前播放URL的信息,包括當前播放器音量大小、播放總時長、當前播放到的時間、當前播放URL在播放隊列中的下標、播放器播放URL隊列數組、剩余播放URL數
-
5、播放器控制接口,開始播放、暫停播放、停止播放、設置播放進度、移動播放指定下標開始播放(實現播放上一條、下一條)
-
6、播放器監聽,通過代理監聽:播放器開始播放、正在播放、當前緩沖進度、結束播放、播放錯誤
</ul> </li>
</ul>
-
錄音,AVAudioRecorder封裝
大概流程:檢測授權 - 創建錄音器 - 配置基本信息 - 準備錄音 - 錄音 - 暫停/停止
-
1、檢測授權
由于錄音需要訪問麥克風,需要在 Info.plist 添加 Privacy - Microphone Usage Description key,但是添加后,系統會首次進入的時候提示,如果拒絕了就什么都不處理,此時需要通過 AVCaptureDevice 手動監聽,如果之前同意授權,那么就正常創建錄音器,否則提示用戶去開啟,具體代碼如下:
- (void)checkAuth{ switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) { case AVAuthorizationStatusAuthorized:{ [self fl_createAudioRecorder]; break; } case AVAuthorizationStatusNotDetermined:{ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { if (granted) { [self fl_createAudioRecorder]; } else{ [self fl_showAuthTip]; } }]; break; } default: [self fl_showAuthTip]; break; } }
- (void)fl_showAuthTip{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"溫馨提示" message:@"您還沒開啟授權麥克風,請打開--> 設置 -- > 隱私 --> 通用等權限設置" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; [alert show]; }
-
2、創建錄音器
在這方法內部,就可以配置默認基本信息(例如音頻支持類型、默認通道數、默認采樣率等等)、創建高精度定時器、準備錄音、監聽相關事件(例如應用程序進入后臺、前臺處理、錄音結束、錄音編碼失敗、錄音被打斷、打斷結束等)
- (void)fl_createAudioRecorder{ NSError error; AVAudioSession audioSession = [AVAudioSession sharedInstance]; // 設置類別,表示該應用同時支持播放和錄音 [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error: &error]; // 啟動音頻會話管理,此時會阻斷后臺音樂的播放. [audioSession setActive:YES error: &error];
// 音頻使用內置揚聲器和麥克風 [audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
self.Recorder = [[AVAudioRecorder alloc] initWithURL:[NSURL fileURLWithPath:[self fl_filePath]] settings:[self fl_recorderSetting] error:&error]; if (error) { [self fl_delegateResponseFailureWithCode:FLAudioRecorderErrorByRecorderIsNotInit]; return; }
[self fl_createTimer];
self.Recorder.delegate = self; // 開啟音量檢測 self.Recorder.meteringEnabled = YES;
[self fl_prepare];
[self fl_addObserver]; }</code></pre> </li>
3、高精度定時器創建
由于 AVAudioRecorder 并沒有提供類似 AVPlayer 的高精度監聽正在播放機制,因此需要手動創建一個監聽,內部計算,從而實現定時結束錄音,此處使用 GCD 來創建定時器。
為啥不用 NSTimer ,首先 NSTimer 只提供創建和銷毀,配合錄音的開始、暫停就需要不斷創建和銷毀,相對來說消耗多點性能,其次回調都是使用 SEL (iOS 10之前),相對來說,沒那么方便;相反,使用 GCD 來創建定時器只需要創建一次,而且很方便地開始( dispatch_resume )、暫停( dispatch_suspend )和停止( dispatch_suspend 同時重置定時計數達到停止效果)定時器,和錄音配合簡直完美
為啥說是高精度,這個其實是相對而言的,一般錄音、播放基本單位都是秒,那么此時錄音定時則0.01s執行一次,就是說,定時器可獲取到小數點后兩位,部分具體代碼如下:
- (void)fl_createTimer{ // 創建定時器對象 self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); // 設置時間間隔 dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC, 0); // 定時器回調 __weak typeof(self) weakSelf = self; dispatch_source_set_event_handler(self.timer, ^{ typeof(self) strongSelf = weakSelf; if (strongSelf.count >= strongSelf.endTime * 100) { strongSelf.count = strongSelf.endTime * 100; [strongSelf fl_stop:nil]; } else{ FL_DELEGATE_RESPONSE(strongSelf.delegate, @selector(fl_audioRecorder:recordingWithCurrentTime:), @[strongSelf,@(strongSelf.count++ / 100)], nil); } }); }
4、開始錄音、暫停錄音、停止錄音
這些API都是直接封裝 AVAudioRecorder 提供的,只是在此基礎上添加一些監聽、狀態更新、錯誤處理,這里談談代理相應如何處理,其他具體處理可查看源碼。
也許大家看上面示例代碼會發現有個C函數 FL_DELEGATE_RESPONSE(<#id delegate#>, <#SEL selector#>, <#NSArray<id> *objects#>, <#^(void)complete#>) ,這個是一個私有方法,專門處理代理回調實現,delegate就是要 response 的
target,selector 就是 response 的方法,objects 就是 selector 所需要的參數,complete是 response 后的操作回調。
一般做法是:
if (self.delegate && [self.delegate respondsToSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:)]) { [self.delegate fl_audioRecorder:self recordingWithCurrentTime:@(strongSelf.count++ / 100)]; }
當然,此時可以利用 performSelector 進行封裝,變成如下:(其中 FLSuppressPerformSelectorLeakWarning 宏是用作忽略編譯器警告)
if (self.delegate && [self.delegate respondsToSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:)]) { FLSuppressPerformSelectorLeakWarning( [self.delegate performSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:) withObject:self withObject:@(strongSelf.count++ / 100)]; ); }
很快就會發現, performSelector 就不適用了,因為某些代理需要傳遞的參數不止兩個,可能是3個、4個或更多,那么此時就有個終極解決辦法,就是通過獲取方法簽名 NSMethodSignature ,然后獲取方法實現 NSInvocation 通過設置 target 、 selector 和 argument ,然后調用 invoke 方法去執行,因此參數可以通過數組傳入,具體實現代碼如下:
id FL_PERFORM_SELECTOR(id target,SEL selector,NSArray <id>* objects){ // 獲取方法簽名 NSMethodSignature *sig = [target methodSignatureForSelector:selector]; if (sig){ NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig]; [invo setTarget:target]; [invo setSelector:selector]; for (NSInteger index = 0; index < objects.count; index ++) { id object = objects[index]; // 參數從下標2開始 [invo setArgument:&object atIndex:index + 2]; } [invo invoke]; if (sig.methodReturnLength) { id anObject; [invo getReturnValue:&anObject]; return anObject; } else { return nil; } } else { return nil; } }
-
播放,AVPlayer封裝
大概流程:創建播放器-設置監聽-開始播放-暫停/停止播放
-
1、創建播放器
通過傳入 URL 字符串 或者 URL 字符串數組,將 URL 添加到內部隊列中,并設置監聽當前的 URL ,核心代碼如下:
- (void)fl_createPlayWithItem:(AVPlayerItem *)item andStartImmediately:(BOOL)startImmediately{ // 銷毀之前的 [self fl_removeObserve]; // 初始化播放器 if (!self.player) { self.player = [AVPlayer playerWithPlayerItem:item]; } else{ [self.player replaceCurrentItemWithPlayerItem:item]; } // 監聽 [self fl_addObserve]; self.playerStatus = Player_Stoping; if (startImmediately) { [self fl_startAtBegining]; } }
-
2、設置監聽
監聽開始播放、正在播放(通過 addPeriodicTimeObserverForInterval 監聽)、緩沖進度(通過 KVO 監聽 AVPlayerItem 的 loadedTimeRanges 屬性)、結束播放、播放失敗、應用進入前后臺、耳機拔插事件,核心代碼如下:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ AVPlayerItem *playerItem = object; if ([keyPath isEqualToString:@"status"]) { AVPlayerStatus status= [[change objectForKey:@"new"] intValue]; if(status == AVPlayerStatusReadyToPlay){ } else if(status == AVPlayerStatusUnknown){ [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByUnknow]; } else if (status == AVPlayerStatusFailed){ [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerStatusFailed]; } } else if([keyPath isEqualToString:@"loadedTimeRanges"]){ NSArray *array = playerItem.loadedTimeRanges; //本次緩沖時間范圍 CMTimeRange timeRange = [array.firstObject CMTimeRangeValue]; float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); //緩沖總長度 NSTimeInterval totalBuffer = startSeconds + durationSeconds; CGFloat bufferProgress = totalBuffer / self.totalTime.doubleValue; [self fl_delegateResponseToSelector:@selector(fl_audioPlayer:cacheToCurrentBufferProgress:) withObject:@[self,@(FL_SAVE_PROGRESS(bufferProgress))] complete:nil]; } else if ([keyPath isEqualToString:@"playbackBufferEmpty"]){ } else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]){ } }
__weak typeof(self) weakSelf = self; self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { typeof(self) strongSelf = weakSelf; CGFloat progress = strongSelf.currentTime.doubleValue / strongSelf.totalTime.doubleValue; [strongSelf fl_delegateResponseToSelector:@selector(fl_audioPlayer:playingToCurrentProgress:withBufferProgress:) withObject:@[strongSelf,@(FL_SAVE_PROGRESS(progress)),strongSelf.bufferProgress] complete:nil]; }];
-
3、播放器管理,開始、暫停、停止、新增播放URL地址、指定播放等
開始、暫停、seek都是針對 AVPlayer 提供的接口進行二次封裝,添加一些事件監聽、狀態更新以及錯誤處理,停止則是通過 pause 和 seek 配合實現,這里主要談談隊列播放的實現。
通過上文可以知道,系統提供的 AVQueuePlayer 能實現隊列播放,只需要傳入一個 AVPlayerItem 數組即可,能實現播放下一條,它是 AVPlayer 的子類,就是說, AVQueuePlayer 只不過是針對 AVPlayer 進行二次封裝來實現隊列播放而已,而且效果不太如意,通過 advanceToNextItem 方法播放下一條,會移除之前的 item ,那么就沒辦法實現播放上一條和指定播放任一條的需求,而且監聽起來十分麻煩,因此自行實現隊列播放
內部主要是通過兩個數組實現, valiableItems 是所有添加進去的 AVPlayerItem 數組,而 lastItems 是剩下需要播放的 AVPlayerItem 數組,當前播放的Item永遠都是 lastItems 的第一個元素,意味著,通過 valiableItems 數組動態修改 lastItems 數組的元素,即可實現播放 valiableItems 數組中的任意一條地址,具體代碼如下:
@property (nonatomic,strong)NSMutableArray<AVPlayerItem *> *valiableItems; @property (nonatomic,strong)NSMutableArray<AVPlayerItem *> *lastItems;
- (void)fl_moveToIndex:(NSInteger)index andStartImmediately:(BOOL)startImmediately{ if (!self.player) { [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerIsNotInit]; return; } if (!self.valiableUrls.count) { [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerNoMoreValiableUrl]; return; }
if (index < 0) { index = 0; } else if (index > self.valiableUrls.count - 1){ index = self.valiableUrls.count - 1; }
// stop current [self fl_stop];
[self.lastItems removeAllObjects]; NSMutableArray <AVPlayerItem >tempArrM = self.valiableItems.mutableCopy; NSIndexSet se = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, tempArrM.count - index)]; [self.lastItems addObjectsFromArray:[tempArrM objectsAtIndexes:se]]; AVPlayerItem firstItem = self.lastItems.firstObject; self.currentUrl = self.valiableUrls[index]; [self fl_createPlayWithItem:firstItem andStartImmediately:startImmediately]; }</code></pre> </li> </ul> </li> </ul>
五、細節點
-
1、傳入URL地址時候,不需要關心是本地還是網絡,內部會自動處理,核心代碼如下:
BOOL FL_ISNETURL(NSString *urlString){ return [[urlString substringToIndex:4] caseInsensitiveCompare:@"http"] == NSOrderedSame || [[urlString substringToIndex:5] caseInsensitiveCompare:@"https"] == NSOrderedSame; }
- (NSURL )fl_getSuitableUrl:(NSString )urlString{
NSURL *url = nil;
if (FL_ISNETURL(urlString)) {
} else{url = [NSURL URLWithString:urlString];
} return url; }</code></pre> </li>url = [NSURL fileURLWithPath:urlString];
-
2、內置一個時間格式轉換,通過C函數 FL_COVERTTIME 傳入時間戳(單位秒),核心代碼如下:
NSString FL_COVERTTIME(CGFloat second){ NSDate date = [NSDate dateWithTimeIntervalSince1970:second]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; if (second/3600 >= 1) {
} else {[formatter setDateFormat:@"HH:mm:ss"];
} NSString showtimeNew = [formatter stringFromDate:date]; return showtimeNew; }</code></pre> </li>[formatter setDateFormat:@"mm:ss"];
3、內置音量控制,通過 MPVolumeView 控制系統聲音來實現,外部只需要修改 currentVolum 屬性即可
/** 當前播放的音量大小(0.0-1.0),注意,播放音頻的時候設置才生效
4、框架內代理方法全部基本數據類型都包裝成NSNumber,方便統一處理,因此外界獲取時,可通過 .doubleValue 獲取
5、統一處理了所有錯誤信息,有一一對應的error,可根據code判斷,具體的code,可查看相應的錯誤代理方法解釋
六、總結
-
1、通過研究錄音播放的上層接口類,了解API的設計思路,可以更好的理解底層實現,以及為日后封裝提供API設計規范
-
2、具體API以及實現、調用方法,可以去 Github clone 代碼,里面有詳細完整的Demo 演示
-
3、Demo中附帶自定義滑動進度條,監聽事件完善,帶有緩沖進度, FLSlider Github 地址 ,詳細使用Demo中也有演示
-
4、如果大家發現上文有哪里說得不對或者有更好的建議,歡迎評論或簡信我!如果你覺得寫得不錯,歡迎關注我!給個like 和 start,謝謝支持
來自:http://www.jianshu.com/p/bc9fe7052e52
- (NSURL )fl_getSuitableUrl:(NSString )urlString{
NSURL *url = nil;
if (FL_ISNETURL(urlString)) {
-
-
-
四、功能封裝實現
-
-
-