[TOC]
音頻會(huì)話
在使用Apple設(shè)備時(shí),我們注意到有些應(yīng)用打開音頻播放時(shí),其他音頻就會(huì)終止,而有些應(yīng)用卻可以同時(shí)使用音頻,這就出現(xiàn)了多種聲音的情況,這些音頻環(huán)境就涉及了音頻會(huì)話內(nèi)容:iOS中,每個(gè)應(yīng)用都有一個(gè)音頻會(huì)話,及AVAudioSession,它屬于AVFoundation框架中,是一種單例模式。
| 會(huì)話類型 | 說明 | 是否要求輸入 | 是否需要輸出 | 是否遵從靜音鍵 |
|---|---|---|---|---|
| AVAudioSessionCategoryAmbient | 混音播放,可以與其他音頻應(yīng)用同時(shí)播放 | 否 | 是 | 是 |
| AVAudioSessionCategorySoloAmbient | 獨(dú)占播放 | 否 | 是 | 是 |
| AVAudioSessionCategoryPlayback | 后臺(tái)播放,也是獨(dú)占的 | 否 | 是 | 否 |
| AVAudioSessionCategoryRecord | 錄音模式,用于錄音時(shí)使用 | 是 | 否 | 否 |
| AVAudioSessionCategoryPlayAndRecord | 播放和錄音,此時(shí)可以錄音也可以播放 | 是 | 是 | 否 |
| AVAudioSessionCategoryAudioProcessing | 硬件解碼音頻,此時(shí)不能播放和錄制 | 否 | 否 | 否 |
| AVAudioSessionCategoryMultiRoute | 多種輸入輸出,例如可以耳機(jī)、USB設(shè)備同時(shí)播放 | 是 | 是 | 否 |
注意:是否遵循靜音鍵表示在播放過程中如果用戶通過硬件設(shè)置為靜音是否能關(guān)閉聲音
例:設(shè)置錄音環(huán)境
NSError *sessionError;
// 設(shè)置音頻會(huì)話為錄音環(huán)境
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&sessionError];
if (sessionError){
NSLog(@"Error creating session: %@",[sessionError description]);
}else{
// 啟動(dòng)該會(huì)話
[[AVAudioSession sharedInstance] setActive:YES error:&sessionError];
}
說明:假如我們?cè)O(shè)置的音頻會(huì)話為獨(dú)占的話(如:后臺(tái)播放),其他正在播放音頻的應(yīng)用就會(huì)被終止播放
音頻的播放
音頻的播放分為音效播放和音樂播放。前者是一些短音頻播放,不需要進(jìn)度、循環(huán)次數(shù)控制。后者是一些較長(zhǎng)的音頻,需要對(duì)進(jìn)度、循環(huán)次數(shù)做精確控制。在iOS中,分別使用AudioToolbox.framework和AVFoundation.framework來完成音效和音頻的播放
[TOC]
音效播放
AudioToolbox.framework是一套基于C語言的框架,使用它來播放音效其本質(zhì)是將短音頻注冊(cè)到系統(tǒng)聲音服務(wù)(System Sound Service)。System Sound Service是一種簡(jiǎn)單、底層的聲音播放服務(wù),但是它本身也存在著一些限制:
- 音頻播放時(shí)間不能超過30s
- 數(shù)據(jù)必須是PCM或者IMA4格式
- 音頻文件必須打包成.caf、.aif、.wav中的一種
使用步驟如下:
- 調(diào)用AudioServicesCreateSystemSoundID函數(shù)來獲取系統(tǒng)聲音ID
- 調(diào)用AudioServicesPlaySystemSound或者AudioServicesPlayAlertSound 方法播放音效(后者帶有震動(dòng)效果)
- 如果需要監(jiān)聽播放完成操作,則使用AudioServicesAddSystemSoundCompletion方法注冊(cè)回調(diào)函數(shù)
例子:
-(void)playAudioWithName:(NSString*)soundName{
NSURL* system_sound_url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:soundName ofType:nil]];
//1.獲取系統(tǒng)聲音ID
SystemSoundID system_sound_id = 0;
AudioServicesCreateSystemSoundID((__bridge CFURLRef)system_sound_url,&system_sound_id);
//需要播放完成之后執(zhí)行某些操作,可以調(diào)用下面方法注冊(cè)一個(gè)播放完成回調(diào)函數(shù)
/* AudioServicesAddSystemSoundCompletion(system_sound_id,
NULL, // uses the main run loop
NULL, // uses kCFRunLoopDefaultMode
soundCompleteCallback, // the name of our custom callback function
NULL // for user data, but we don't need to do that in this case, so we just pass NULL
);
*/
//2.播放音頻
// AudioServicesPlaySystemSound(system_sound_id);//播放音效
AudioServicesPlayAlertSound(system_sound_id);//播放并震動(dòng)
}
void soundCompleteCallback(SystemSoundID soundID,void * userData){
NSLog(@"播放完成");
//do what you want to do
AudioServicesDisposeSystemSoundID(soundID);
}
補(bǔ)充:獲取系統(tǒng)音效
_systemSounds = [NSMutableArray array];
// 讀取文件系統(tǒng)
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *directoryURL = [NSURL URLWithString:@"/System/Library/Audio/UISounds"];
NSArray *keys = [NSArray arrayWithObject:NSURLIsDirectoryKey];
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:directoryURL
includingPropertiesForKeys:keys
options:0
errorHandler:^(NSURL *url, NSError *error) {
return YES;
}];
for (NSURL *url in enumerator) {
NSError *error;
NSNumber *isDirectory = nil;
if (! [url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error]) {
} else if (![isDirectory boolValue]) {
SystemSoundID soundID;
AudioServicesCreateSystemSoundID((__bridge_retained CFURLRef)url, &soundID);
// 音效的數(shù)據(jù)模型
SoundInfomation *sound = [[SoundInfomation alloc] init];
sound.soundID = soundID;
sound.soundUrl = url;
sound.soundName = url.lastPathComponent;
[_systemSounds addObject:sound];
}
}
[TOC]
音樂播放
如果播放較大的音頻或者要對(duì)音頻有精確的控制則System Sound Service可能就很難滿足實(shí)際需求了,通常這種情況會(huì)選擇使用AVFoundation.framework中的AVAudioPlayer來實(shí)現(xiàn)。AVAudioPlayer可以看成一個(gè)播放器,它支持多種音頻格式,而且能夠進(jìn)行進(jìn)度、音量、播放速度等控制
| 屬性 | 說明 |
|---|---|
| @property(readonly, getter=isPlaying) BOOL playing | 是否正在播放,只讀 |
| @property(readonly) NSUInteger numberOfChannels | 音頻聲道數(shù),只讀 |
| @property(readonly) NSTimeInterval duration | 音頻時(shí)長(zhǎng) |
| @property(readonly) NSURL *url | 音頻文件路徑,只讀 |
| @property(readonly) NSData *data | 音頻數(shù)據(jù),只讀 |
| @property float pan | 立體聲平衡,如果為-1.0則完全左聲道,如果0.0則左右聲道平衡,如果為1.0則完全為右聲道 |
| @property float volume | 音量大小,范圍0-1.0 |
| @property BOOL enableRate | 是否允許改變播放速率 |
| @property float rate | 播放速率,范圍0.5-2.0,如果為1.0則正常播放,如果要修改播放速率則必須設(shè)置enableRate為YES |
| @property NSTimeInterval currentTime | 當(dāng)前播放時(shí)長(zhǎng) |
| @property(readonly) NSTimeInterval deviceCurrentTime | 輸出設(shè)備播放音頻的時(shí)間,注意如果播放中被暫停此時(shí)間也會(huì)繼續(xù)累加 |
| @property NSInteger numberOfLoops | 循環(huán)播放次數(shù),如果為0則不循環(huán),如果小于0則無限循環(huán),大于0則表示循環(huán)次數(shù) |
| @property(readonly) NSDictionary *settings | 音頻播放設(shè)置信息,只讀 |
| @property(getter=isMeteringEnabled) BOOL meteringEnabled | 是否啟用音頻測(cè)量,默認(rèn)為NO,一旦啟用音頻測(cè)量可以通過updateMeters方法更新測(cè)量值 |
| 對(duì)象方法 | 說明 |
|---|---|
| -(instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError | 使用文件URL初始化播放器,注意這個(gè)URL不能是HTTP URL,AVAudioPlayer不支持加載網(wǎng)絡(luò)媒體流,只能播放本地文件,如果需要播放網(wǎng)絡(luò)URL,可以嘗試AVPlayer |
| -(BOOL)prepareToPlay | 加載音頻文件到緩沖區(qū),注意即使在播放之前音頻文件沒有加載到緩沖區(qū)程序也會(huì)隱式調(diào)用此方法。 |
| -(BOOL)play | 播放音頻文件 |
| -(BOOL)playAtTime:(NSTimeInterval)time | 在指定的時(shí)間開始播放音頻 |
| -(void)pause | 暫停播放 |
| -(void)stop | 停止播放 |
| -(void)updateMeters | 更新音頻測(cè)量值,注意如果要更新音頻測(cè)量值必須設(shè)置meteringEnabled為YES,通過音頻測(cè)量值可以即時(shí)獲得音頻分貝等信息 |
| -(float)peakPowerForChannel:(NSUInteger)channelNumber | 獲得指定聲道的分貝峰值,注意如果要獲得分貝峰值必須在此之前調(diào)用updateMeters方法 |
| -(float)averagePowerForChannel:(NSUInteger)channelNumber | 獲得指定聲道的分貝平均值,注意如果要獲得分貝平均值必須在此之前調(diào)用updateMeters方法 |
| 代理方法 | 說明 |
|---|---|
| -(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag | 音頻播放完成 |
| -(void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error | 音頻解碼發(fā)生錯(cuò)誤 |
| -(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player | 播放被打斷 |
| -(void)audioPlayerEndInterruption:(AVAudioPlayer *)player | 結(jié)束打斷 |
使用步驟:
- 初始化AVAudioPlayer對(duì)象,此時(shí)需要指定本地文件路徑
- 設(shè)置播放器屬性,例如重復(fù)次數(shù)、音量大小等
- 調(diào)用play方法播放
示例:
-(void)playAudioWithUrl:(NSURL*)url{
NSError *error=nil;
// 1、初始化
_audioPlayer=[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
// 2、設(shè)置相關(guān)屬性
_audioPlayer.numberOfLoops=0; // 設(shè)置為0不循環(huán)
_audioPlayer.delegate = self;
if (error) {
NSLog(@"創(chuàng)建播放器過程中發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
}
if (![_audioPlayer isPlaying]) {
//解決音量小的問題
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *err = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayback error:&err];
// 3、播放
[_audioPlayer play]; // 播放音頻
}
}
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
NSLog(@"音樂播放完成...");
}
[TOC]
音頻錄制
在AVFoundation框架中還要一個(gè)AVAudioRecorder類專門處理錄音操作,它同樣支持多種音頻格式。與AVAudioPlayer類似,你完全可以將它看成是一個(gè)錄音機(jī)控制類
| 屬性 | 說明 |
|---|---|
| @property(readonly, getter=isRecording) BOOL recording | 是否正在錄音,只讀 |
| @property(readonly) NSURL *url | 錄音文件地址,只讀 |
| @property(readonly) NSDictionary *settings | 錄音文件設(shè)置,只讀 |
| @property(readonly) NSTimeInterval currentTime | 錄音時(shí)長(zhǎng),只讀,注意僅僅在錄音狀態(tài)可用 |
| @property(readonly) NSTimeInterval deviceCurrentTime | 輸入設(shè)置的時(shí)間長(zhǎng)度,只讀,注意此屬性一直可訪問 |
| @property(getter=isMeteringEnabled) BOOL meteringEnabled | 是否啟用錄音測(cè)量,如果啟用錄音測(cè)量可以獲得錄音分貝等數(shù)據(jù)信息 |
| 對(duì)象方法 | 說明 |
|---|---|
| -(instancetype)initWithURL:(NSURL *)url settings:(NSDictionary *)settings error:(NSError **)outError | 錄音機(jī)對(duì)象初始化方法,注意其中的url必須是本地文件url,settings是錄音格式、編碼等設(shè)置 |
| -(BOOL)prepareToRecord | 準(zhǔn)備錄音,主要用于創(chuàng)建緩沖區(qū),如果不手動(dòng)調(diào)用,在調(diào)用record錄音時(shí)也會(huì)自動(dòng)調(diào)用 |
| -(BOOL)record | 開始錄音/恢復(fù)錄音 |
| -(BOOL)recordAtTime:(NSTimeInterval)time | 在指定將來的某個(gè)時(shí)刻開始錄音 |
| -(BOOL)recordForDuration:(NSTimeInterval) duration | 指定的時(shí)長(zhǎng)開始錄音,如60' |
| -(BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration | 在指定的時(shí)間開始錄音,并指定錄音時(shí)長(zhǎng) |
| -(void)pause | 暫停錄音 |
| -(void)stop | 停止錄音 |
| -(BOOL)deleteRecording | 刪除錄音,注意要?jiǎng)h除錄音此時(shí)錄音機(jī)必須處于停止?fàn)顟B(tài) |
| -(void)updateMeters | 更新測(cè)量數(shù)據(jù),注意只有meteringEnabled為YES此方法才可用 |
| -(float)peakPowerForChannel:(NSUInteger)channelNumber | 指定通道的測(cè)量峰值,注意只有調(diào)用完updateMeters才有值 |
| -(float)averagePowerForChannel:(NSUInteger)channelNumber | 指定通道的測(cè)量平均值,注意只有調(diào)用完updateMeters才有值 |
| 代理方法 | 說明 |
|---|---|
| -(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag | 完成錄音 |
| -(void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error | 錄音編碼發(fā)生錯(cuò)誤 |
| -(void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder | 錄音被打斷 |
| -(void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder | 錄音打斷結(jié)束 |
AVAudioRecorder很多屬性和方法跟AVAudioPlayer都是類似的,但是它的創(chuàng)建有所不同,在創(chuàng)建錄音機(jī)時(shí)除了指定路徑外還必須指定錄音設(shè)置信息,因?yàn)殇浺魴C(jī)必須知道錄音文件的格式、采樣率、通道數(shù)、每個(gè)采樣點(diǎn)的位數(shù)等信息,但是也并不是所有的信息都必須設(shè)置,通常只需要幾個(gè)常用設(shè)置
使用步驟:
- 設(shè)置音頻會(huì)話類型為AVAudioSessionCategoryRecord,錄音模式
- 創(chuàng)建錄音機(jī)AVAudioRecorder,指定錄音保存的路徑并且設(shè)置錄音屬性
- 調(diào)用record開始錄制
示例:
-(void)setupRecorder{
//設(shè)置音頻會(huì)話
NSError *sessionError;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&sessionError];
if (sessionError){
NSLog(@"Error creating session: %@",[sessionError description]);
}else{
[[AVAudioSession sharedInstance] setActive:YES error:&sessionError];
}
//錄音設(shè)置
//創(chuàng)建錄音文件保存路徑
NSURL *url = [self getSavePath];
//創(chuàng)建錄音機(jī)
NSError *error = nil;
_audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:self.setting error:&error];
if (error) {
NSLog(@"創(chuàng)建錄音機(jī)對(duì)象時(shí)發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
}
_audioRecorder.delegate = self;
_audioRecorder.meteringEnabled = YES;//如果要監(jiān)控聲波則必須設(shè)置為YES
[_audioRecorder prepareToRecord];
if (![_audioRecorder isRecording]) {
[_audioRecorder record];//首次使用應(yīng)用時(shí)如果調(diào)用record方法會(huì)詢問用戶是否允許使用麥克風(fēng)
}
}
// !!!: 錄音設(shè)置
-(NSDictionary *)setting{
if (_setting==nil) {
NSMutableDictionary *setting = [NSMutableDictionary dictionary];
//錄音格式
[setting setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
//采樣率,8000/11025/22050/44100/96000(影響音頻的質(zhì)量),8000是電話采樣率
[setting setObject:@(22050) forKey:AVSampleRateKey];
//通道 , 1/2
[setting setObject:@(2) forKey:AVNumberOfChannelsKey];
//采樣點(diǎn)位數(shù),分為8、16、24、32, 默認(rèn)16
[setting setObject:@(16) forKey:AVLinearPCMBitDepthKey];
//是否使用浮點(diǎn)數(shù)采樣
[setting setObject:@(YES) forKey:AVLinearPCMIsFloatKey];
// 錄音質(zhì)量
[setting setObject:@(AVAudioQualityHigh) forKey:AVEncoderAudioQualityKey];
//....其他設(shè)置等
}
return _setting;
}
[TOC]
音頻管理
下面是筆者自己寫的一個(gè)音頻管理類??梢赃M(jìn)行音頻錄制、播放、獲取音頻信息、管理音頻文件等功能
.h 文件
//
// AudioManager.h
// ExeToExp
//
// Created by LOLITA on 17/3/28.
// Copyright ? 2017年 LOLITA. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@protocol AudioManagerDelegate;
@interface AudioManager : NSObject
@property (nonatomic,weak) id <AudioManagerDelegate> delegate;
+(instancetype)sharedInstance;
// !!!: 開始錄制
-(void)startRecord;
// !!!: 暫停錄制
-(void)pauseRecord;
// !!!: 恢復(fù)錄制
-(void)resumeRecord;
// !!!: 停止錄制
-(void)stopRecord;
// !!!: 取消當(dāng)前錄制
-(void)cancelRecord;
// !!!: 播放語音
-(void)playAudioWithUrl:(NSURL*)url;
// !!!: 停止語音播放
-(void)stopPlay;
// !!!: 暫停語音播放
-(void)pausePlay;
// !!!: 恢復(fù)語音播放
-(void)resumePlay;
// !!!: 獲取當(dāng)前錄制文件的路徑
-(NSURL*)recordCurrentAudioFile;
// !!!: 獲取語音時(shí)長(zhǎng)
-(float)durationWithAudio:(NSURL *)audioUrl;
// !!!: 刪除本地音頻文件下所有文件
-(void)removeAllAudioFile;
// !!!: 刪除本地指定音頻文件
-(void)removeAudioFile:(NSURL*)url;
// !!!: 刪除指定后綴的文件,如“.wav”,“.caf”
-(void)removeFileSuffixList:(NSArray<NSString*>*)suffixList filePath:(NSString*)path;
@end
@protocol AudioManagerDelegate <NSObject>
@optional
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfullyFlag:(BOOL)flag; // 錄制完成
-(void)audioPowerChange:(CGFloat)power; // 音量
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag; // 播放完成
@end
.m 文件
//
// AudioManager.m
// ExeToExp
//
// Created by LOLITA on 17/3/28.
// Copyright ? 2017年 LOLITA. All rights reserved.
//
#define kAudioFolder @"AudioFolder" // 音頻文件夾
#import "AudioManager.h"
@interface AudioManager ()<AVAudioRecorderDelegate,AVAudioPlayerDelegate>
@property (nonatomic,strong) AVAudioRecorder *audioRecorder; // 錄音機(jī)
@property (nonatomic,strong) AVAudioPlayer *audioPlayer; // 音頻播放器
@property (strong ,nonatomic) NSDictionary *setting; // 錄音機(jī)的設(shè)置
@property (copy ,nonatomic) NSString *audioDir; // 錄音文件夾路徑
@property (nonatomic,strong) NSTimer *timer; // 錄音聲波監(jiān)控
@property (copy ,nonatomic) NSString *filename; // 記錄當(dāng)前文件名
@property (assign ,nonatomic) BOOL cancelCurrentRecord; // 取消當(dāng)前錄制
@end;
@implementation AudioManager
+(instancetype)sharedInstance{
static AudioManager *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[AudioManager alloc] init];
});
return instance;
}
#pragma mark - <************************** 一些初始化 **************************>
// !!!: 配置錄音機(jī)
-(void)setupRecorder{
//設(shè)置音頻會(huì)話
NSError *sessionError;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&sessionError];
if (sessionError){
NSLog(@"Error creating session: %@",[sessionError description]);
}else{
[[AVAudioSession sharedInstance] setActive:YES error:&sessionError];
}
//錄音設(shè)置
//創(chuàng)建錄音文件保存路徑
NSURL *url = [self getSavePath];
//創(chuàng)建錄音機(jī)
NSError *error = nil;
_audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:self.setting error:&error];
_audioRecorder.delegate = self;
_audioRecorder.meteringEnabled = YES;//如果要監(jiān)控聲波則必須設(shè)置為YES
[_audioRecorder prepareToRecord];
if (error) {
NSLog(@"創(chuàng)建錄音機(jī)對(duì)象時(shí)發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
}
}
// !!!: 錄音聲波監(jiān)
-(NSTimer *)timer{
if (!_timer) {
_timer=[NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(powerChange) userInfo:nil repeats:YES];
}
return _timer;
}
// !!!: 錄音設(shè)置
-(NSDictionary *)setting{
if (_setting==nil) {
NSMutableDictionary *setting = [NSMutableDictionary dictionary];
//錄音格式
[setting setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
//采樣率,8000/11025/22050/44100/96000(影響音頻的質(zhì)量),8000是電話采樣率
[setting setObject:@(22050) forKey:AVSampleRateKey];
//通道 , 1/2
[setting setObject:@(2) forKey:AVNumberOfChannelsKey];
//采樣點(diǎn)位數(shù),分為8、16、24、32, 默認(rèn)16
[setting setObject:@(16) forKey:AVLinearPCMBitDepthKey];
//是否使用浮點(diǎn)數(shù)采樣
[setting setObject:@(YES) forKey:AVLinearPCMIsFloatKey];
// 錄音質(zhì)量
[setting setObject:@(AVAudioQualityHigh) forKey:AVEncoderAudioQualityKey];
//....其他設(shè)置等
}
return _setting;
}
// !!!: 錄音文件夾
-(NSString *)audioDir{
if (_audioDir==nil) {
_audioDir = NSTemporaryDirectory();
_audioDir = [_audioDir stringByAppendingPathComponent:kAudioFolder];
BOOL isDir = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL existed = [fileManager fileExistsAtPath:_audioDir isDirectory:&isDir];
if (!(isDir == YES && existed == YES)){
[fileManager createDirectoryAtPath:_audioDir withIntermediateDirectories:YES attributes:nil error:nil];
}
}
return _audioDir;
}
#pragma mark - <************************** 事件 **************************>
// !!!: 開始錄制
-(void)startRecord{
[self setupRecorder];
if (![self.audioRecorder isRecording]) {
[self.audioRecorder record];//首次使用應(yīng)用時(shí)如果調(diào)用record方法會(huì)詢問用戶是否允許使用麥克風(fēng)
// [self.audioRecorder recordForDuration:60]; // 錄音時(shí)長(zhǎng)
self.timer.fireDate=[NSDate distantPast];
}
}
// !!!: 暫停錄制
-(void)pauseRecord{
if ([self.audioRecorder isRecording]) {
[self.audioRecorder pause];
self.timer.fireDate=[NSDate distantFuture];
}
}
// !!!: 恢復(fù)錄制
-(void)resumeRecord{
if (![self.audioRecorder isRecording]) {
[self.audioRecorder record];
self.timer.fireDate=[NSDate distantPast];
}
}
// !!!: 停止錄制
-(void)stopRecord{
[self.audioRecorder stop];
self.timer.fireDate=[NSDate distantFuture];
}
// !!!: 取消當(dāng)前錄制
-(void)cancelRecord{
self.cancelCurrentRecord = YES;
[self stopRecord];
if ([self.audioRecorder deleteRecording]) {
NSLog(@"刪除錄音文件!");
}
}
// !!!: 播放音頻文件
-(void)playAudioWithUrl:(NSURL*)url{
//語音播放
NSError *error=nil;
_audioPlayer=[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
_audioPlayer.numberOfLoops=0; // 設(shè)置為0不循環(huán)
_audioPlayer.delegate = self;
if (error) {
NSLog(@"創(chuàng)建播放器過程中發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
}
if (![_audioPlayer isPlaying]) {
//解決音量小的問題
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *err = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayback error:&err];
[_audioPlayer play]; // 播放音頻
}
}
// !!!: 停止播放語音
-(void)stopPlay{
[self.audioPlayer stop];
}
// !!!: 暫停語音
-(void)pausePlay{
[self.audioPlayer pause];
}
// !!!: 恢復(fù)語音
-(void)resumePlay{
[self.audioPlayer play];
}
#pragma mark - <************************** 獲取數(shù)據(jù) **************************>
// !!!: 獲取錄音保存路徑
-(NSURL*)getSavePath{
self.filename = [NSString stringWithFormat:@"audio_%@.wav",[self getDateString]];
NSString* fileUrlString = [self.audioDir stringByAppendingPathComponent:self.filename];
NSURL *url = [NSURL fileURLWithPath:fileUrlString];
return url;
}
// !!!: 返回音頻文件地址
-(NSURL *)recordCurrentAudioFile{
NSString* fileUrlString = [self.audioDir stringByAppendingPathComponent:self.filename];
NSURL *url = [NSURL fileURLWithPath:fileUrlString];
return url;
}
// !!!: 獲取語音時(shí)長(zhǎng)
-(float)durationWithAudio:(NSURL *)audioUrl{
AVURLAsset* audioAsset = [AVURLAsset URLAssetWithURL:audioUrl options:nil];
CMTime audioDuration = audioAsset.duration;
float audioDurationSeconds = CMTimeGetSeconds(audioDuration);
return audioDurationSeconds;
}
// !!!: 刪除所有文件夾
-(void)removeAllAudioFile{
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager removeItemAtPath:self.audioDir error:nil]) {
NSLog(@"刪除文件夾成功?。?);
}
}
// !!!: 刪除指定文件
-(void)removeAudioFile:(NSURL *)url{
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager removeItemAtPath:url.path error:nil]) {
NSLog(@"刪除錄音文件成功??!");
}
}
// !!!: 刪除指定后綴的文件
-(void)removeFileSuffixList:(NSArray<NSString *> *)suffixList filePath:(NSString *)path{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *contentOfFolder = [fileManager contentsOfDirectoryAtPath:path error:NULL];
for (NSString *aPath in contentOfFolder) {
NSString * fullPath = [path stringByAppendingPathComponent:aPath];
BOOL isDir = NO;
if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDir]) {
if (isDir == YES) {
// 是文件夾,則繼續(xù)遍歷
[self removeFileSuffixList:suffixList filePath:fullPath];
}
else{
NSLog(@"file-:%@", aPath);
for (NSString* suffix in suffixList) {
if ([aPath hasSuffix:suffix]) {
if ([fileManager removeItemAtPath:fullPath error:nil]) {
NSLog(@"刪除文件成功!!");
}
}
}
}
}
}
}
#pragma mark - <************************** 代理方法 **************************>
// !!!: 錄音代理事件
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{
if (self.cancelCurrentRecord) {
self.cancelCurrentRecord = NO;
NSLog(@"取消錄制!");
}
else{
if (self.delegate&&[self.delegate respondsToSelector:@selector(audioRecorderDidFinishRecording:successfullyFlag:)]) {
[self.delegate audioRecorderDidFinishRecording:recorder successfullyFlag:flag];
}
NSLog(@"錄制完成!");
}
}
// !!!: 播放語音代理事件
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
if (self.delegate&&[self.delegate respondsToSelector:@selector(audioPlayerDidFinishPlaying:successfully:)]) {
[self.delegate audioPlayerDidFinishPlaying:player successfully:flag];
}
NSLog(@"播放完成!");
}
#pragma mark - <************************** 私有方法 **************************>
// !!!: 獲取時(shí)刻名稱
-(NSString*)getDateString{
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSInteger unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday |
NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *comps = [calendar components:unitFlags fromDate:[NSDate date]];
NSInteger year = [comps year];
NSInteger month = [comps month];
NSInteger day = [comps day];
NSInteger hour = [comps hour];
NSInteger min = [comps minute];
NSInteger sec = [comps second];
NSString* formatString = @"%d%02d%02d%02d%02d%02d";
return [NSString stringWithFormat:formatString, year, month, day, hour, min, sec];
}
// !!!: 錄音聲波狀態(tài)設(shè)置
-(void)powerChange{
[self.audioRecorder updateMeters];//更新測(cè)量值
float power = [self.audioRecorder averagePowerForChannel:0];//取得第一個(gè)通道的音頻,注意音頻強(qiáng)度范圍時(shí)-160到0
CGFloat progress = power+160.0;
NSLog(@"音頻強(qiáng)度:%f",power);
if (self.delegate&&[self.delegate respondsToSelector:@selector(audioPowerChange:)]) {
[self.delegate audioPowerChange:progress];
}
}
-(void)dealloc{
[self removeAllAudioFile];
}
@end
[TOC]
補(bǔ)充:音頻隊(duì)列服務(wù)
音頻隊(duì)列服務(wù)Audio Queue Services屬于AudioToolbox框架,使用音頻隊(duì)列服務(wù)可以做到音頻流式播放和錄制
首先看一下錄音音頻服務(wù)隊(duì)列:

一個(gè)音頻服務(wù)隊(duì)列Audio Queue有三部分組成:
三個(gè)緩沖器Buffers:每個(gè)緩沖器都是一個(gè)存儲(chǔ)音頻數(shù)據(jù)的臨時(shí)倉庫。
一個(gè)緩沖隊(duì)列Buffer Queue:一個(gè)包含音頻緩沖器的有序隊(duì)列。
一個(gè)回調(diào)Callback:一個(gè)自定義的隊(duì)列回調(diào)函數(shù)。
聲音通過輸入設(shè)備進(jìn)入緩沖隊(duì)列中,首先填充第一個(gè)緩沖器;當(dāng)?shù)谝粋€(gè)緩沖器填充滿之后自動(dòng)填充下一個(gè)緩沖器,同時(shí)會(huì)調(diào)用回調(diào)函數(shù);在回調(diào)函數(shù)中需要將緩沖器中的音頻數(shù)據(jù)寫入磁盤,同時(shí)將緩沖器放回到緩沖隊(duì)列中以便重用。下面是Apple官方關(guān)于音頻隊(duì)列服務(wù)的流程示意圖:

類似的,看一下音頻播放緩沖隊(duì)列,其組成部分和錄音緩沖隊(duì)列類似。

但是在音頻播放緩沖隊(duì)列中,回調(diào)函數(shù)調(diào)用的時(shí)機(jī)不同于音頻錄制緩沖隊(duì)列,流程剛好相反。將音頻讀取到緩沖器中,一旦一個(gè)緩沖器填充滿之后就放到緩沖隊(duì)列中,然后繼續(xù)填充其他緩沖器;當(dāng)開始播放時(shí),則從第一個(gè)緩沖器中讀取音頻進(jìn)行播放;一旦播放完之后就會(huì)觸發(fā)回調(diào)函數(shù),開始播放下一個(gè)緩沖器中的音頻,同時(shí)填充第一個(gè)緩沖器放;填充滿之后再次放回到緩沖隊(duì)列。下面是詳細(xì)的流程:

當(dāng)然,要明白音頻隊(duì)列服務(wù)的原理并不難,問題是如何實(shí)現(xiàn)這個(gè)自定義的回調(diào)函數(shù),這其中我們有大量的工作要做,控制播放狀態(tài)、處理異常中斷、進(jìn)行音頻編碼等等。由于牽扯內(nèi)容過多,而且不是本文目的,如果以后有時(shí)間將另開一篇文章重點(diǎn)介紹,目前有很多第三方優(yōu)秀框架可以直接使用,例如AudioStreamer、FreeStreamer。(前者當(dāng)前只有非ARC版本)
[TOC]