ps:文章內(nèi)容的代碼部分润努,由于不便暴露業(yè)務(wù)邏輯情连,可能會有部分刪減,但是主體功能基本保留
背景
這段時間應(yīng)公司業(yè)務(wù)需求炭菌,要在項目中添加錄音功能,要求訂單(具體是什么訂單逛漫,不便透露)全程實時錄音并轉(zhuǎn)碼存儲黑低,訂單完成之后,上傳至服務(wù)器保存。
調(diào)研過程
- 音頻文件的相關(guān)知識
- 使用系統(tǒng)提供的API調(diào)用錄音功能
- 音頻文件如何優(yōu)化克握、壓縮蕾管、存儲
- 影響正常使用錄音功能的因素
踩坑過程
音頻文件的相關(guān)知識
- 文件格式(不同的文件格式,可保存不同的編碼格式的文件)
- wav:
特點:音質(zhì)最好的格式菩暗,對應(yīng)PCM編碼
適用:多媒體開發(fā)掰曾,保存音樂和音效素材- mp3:
特點:音質(zhì)好,壓縮比比較高停团,被大量軟件和硬件支持
適用:適合用于比較高要求的音樂欣賞- caf:
特點:適用于幾乎iOS中所有的編碼格式
- 編碼格式
- PCM
PCM:脈沖編碼調(diào)制旷坦,是一種非壓縮音頻數(shù)字化技術(shù),是一種未壓縮的原音重現(xiàn)佑稠,數(shù)字模式下秒梅,音頻的初始化信號是PCM- MP3
- AAC
AAC:其實是“高級音頻編碼(advanced audio coding)”的縮寫,他是被設(shè)計用來取代MPC格式的舌胶。- HE-AAC
HE-AAC是AAC的一個超集捆蜀,這個“High efficiency”,HE-AAC是專門為低比特率所優(yōu)化的一種音頻編碼格式幔嫂。- AMR
AMR全稱是“Adaptive Multi-Rate”辆它,它也是另一個專門為“說話(speech)”所優(yōu)化的編碼格式,也是適合低比特率環(huán)境下采用履恩。- ALAC
它全稱是“Apple Lossless”锰茉,這是一種沒有任何質(zhì)量損失的音頻編碼方式,也就是我們說的無損壓縮似袁。- IMA4
IMA4:這是一個在16-bit音頻文件下按照4:1的壓縮比來進(jìn)行壓縮的格式洞辣。
- 影響音頻文件大小的因素
- 采樣頻率
采樣頻率是指單位時間內(nèi)的采樣次數(shù)。采樣頻率越大昙衅,采樣點之間的間隔就越小扬霜,數(shù)字化后得到的聲音就越逼真,但相應(yīng)的數(shù)據(jù)量就越大而涉。- 采樣位數(shù)
采樣位數(shù)是記錄每次采樣值數(shù)值大小的位數(shù)著瓶。采樣位數(shù)通常有8bits或16bits兩種,采樣位數(shù)越大啼县,所能記錄聲音的變化度就越細(xì)膩材原,相應(yīng)的數(shù)據(jù)量就越大。- 聲道數(shù)
聲道數(shù)是指處理的聲音是單聲道還是立體聲季眷。單聲道在聲音處理過程中只有單數(shù)據(jù)流余蟹,而立體聲則需要左、右聲道的兩個數(shù)據(jù)流子刮。顯然威酒,立體聲的效果要好窑睁,但相應(yīng)的數(shù)據(jù)量要比單聲道的數(shù)據(jù)量加倍。- 時長
- 計算
數(shù)據(jù)量(字節(jié)/秒)=(采樣頻率(Hz)× 采樣位數(shù)(bit)× 聲道數(shù))/ 8
總大小=數(shù)據(jù)量 x 時長(秒)
使用系統(tǒng)提供的API調(diào)用錄音功能
申請訪問權(quán)限葵孤,在plist文件中加入
<key>NSMicrophoneUsageDescription</key> <string>App需要您的同意,才能訪問麥克風(fēng)</string>
導(dǎo)入頭文件
#import <AVFoundation/AVFoundation.h>
設(shè)置AVAudioSession請參照iOS音頻掌柜
- (void)prepareRecord:(void(^)(BOOL granted))completeHandler {
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
NSLog(@"Error creating session: %@", [error description]);
//設(shè)置為錄音和語音播放模式担钮,支持邊錄音邊使用揚聲器,與其他應(yīng)用同時使用麥克風(fēng)和揚聲器
[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions: AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers error:&error];
NSLog(@"Error creating session: %@", [error description]);
[session setActive:YES error:nil];
//判斷是否授權(quán)錄音
[session requestRecordPermission:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
completeHandler(granted);
});
}];
}
- 錄音設(shè)置
- (NSDictionary *)audioSetting {
static NSMutableDictionary *setting = nil;
if (setting == nil) {
setting = [NSMutableDictionary dictionary];
setting[AVFormatIDKey] = @(kAudioFormatLinearPCM);
//#define AVSampleRate 8000
setting[AVSampleRateKey] = @(AVSampleRate);
//由于需要壓縮為MP3格式尤仍,所以此處必須為雙聲道
setting[AVNumberOfChannelsKey] = @(2);
setting[AVLinearPCMBitDepthKey] = @(16);
setting[AVEncoderAudioQualityKey] = @(AVAudioQualityMin);
}
return setting;
}
- 文件緩存目錄
//獲取緩存文件的根目錄
- (NSString *)recordCacheDirectory {
static NSString *directory = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *docDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *cacheDir = [docDir stringByAppendingPathComponent:@"緩存文件夾名字"];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir = NO;
BOOL existed = [fm fileExistsAtPath:cacheDir isDirectory:&isDir];
if (!(isDir && existed)) {
[fm createDirectoryAtPath:cacheDir withIntermediateDirectories:YES attributes:nil error:nil];
}
directory = cacheDir;
});
return directory;
}
//錄音文件的緩存路徑
- (NSString *)recordFilePath {
static NSString *path = nil;
if (path == nil) {
path = [[self recordCacheDirectory] stringByAppendingPathComponent:"錄音文件名稱"];
}
return path;
}
- 開始錄音
//開始錄音
- (void)startRecord {
[self prepareRecord:^(BOOL granted) {
if (granted) {
[self _startRecord];
}else {
//進(jìn)行鑒權(quán)申請等操作
}
}];
}
- (void)_startRecord {
NSDictionary *setting = [self audioSetting];
NSString *path = [self recordFilePath];
NSURL *url = [NSURL URLWithString:path];
NSError *error = nil;
self.audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error];
if (error) {
NSLog(@"創(chuàng)建錄音機(jī)時發(fā)生錯誤箫津,信息:%@",error.localizedDescription);
}else {
self.audioRecorder.delegate = self;
self.audioRecorder.meteringEnabled = YES;
[self.audioRecorder prepareToRecord];
[self.audioRecorder record];
NSLog(@"錄音開始");
}
}
音頻文件如何優(yōu)化、壓縮宰啦、存儲
- 優(yōu)化
鑒于項目中對音頻的品質(zhì)要求不是太高苏遥,所以要求最終上傳的音頻文件盡可能小(經(jīng)協(xié)商使用最終保存的文件格式為mp3绑莺,其實amr更好)暖眼,所以我們所使用的音頻采樣率為8000
setting[AVEncoderAudioQualityKey] = @(AVAudioQualityMin);
采樣位數(shù)為16(如果為8的話,最終轉(zhuǎn)碼的mp3的語音會嚴(yán)重失真)
setting[AVLinearPCMBitDepthKey] = @(16);
- 壓縮
最終保存的文件為mp3纺裁,所以需要將caf格式的音頻壓縮為mp3格式诫肠,這部分內(nèi)容請參照Lame開源庫
下面貼一部分實現(xiàn)代碼,可能與上述Lame開源庫文章中有所不同
//接上面的_startRecord方法
- (void)_startRecord {
NSDictionary *setting = [self audioSetting];
NSString *path = [self recordFilePath];
NSURL *url = [NSURL URLWithString:path];
NSError *error = nil;
self.audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error];
if (error) {
NSLog(@"創(chuàng)建錄音機(jī)時發(fā)生錯誤欺缘,信息:%@",error.localizedDescription);
}else {
self.audioRecorder.delegate = self;
self.audioRecorder.meteringEnabled = YES;
[self.audioRecorder prepareToRecord];
[self.audioRecorder record];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self conventToMp3];
});
NSLog(@"錄音開始");
}
}
- (void)conventToMp3 {
NSString *cafPath = [self recordFilePath];
NSString *mp3Path = @"目標(biāo)文件路徑";
@try {
int read, write;
FILE *pcm = fopen([cafPath cStringUsingEncoding:NSASCIIStringEncoding], "rb");
fseek(pcm, 4*1024, SEEK_CUR);
FILE *mp3 = fopen([mp3Path cStringUsingEncoding:NSASCIIStringEncoding], "wb");
const int PCM_SIZE = 8192;
const int MP3_SIZE = 8192;
short int pcm_buffer[PCM_SIZE*2];
unsigned char mp3_buffer[MP3_SIZE];
lame_t lame = lame_init();
lame_set_in_samplerate(lame, AVSampleRate);
lame_set_out_samplerate(lame, AVSampleRate);
lame_set_num_channels(lame, 1);
lame_set_mode(lame, 1);
lame_set_brate(lame, 16);
lame_set_quality(lame, 7);
lame_set_VBR(lame, vbr_default);
lame_init_params(lame);
long currentPosition;
do {
currentPosition = ftell(pcm); //文件讀到當(dāng)前位置
long startPosition = ftell(pcm); //起始點
fseek(pcm, 0, SEEK_END); //將文件指針指向結(jié)束為止
long endPosition = ftell(pcm); //結(jié)束點
long length = endPosition - startPosition; //獲取文件長度
fseek(pcm, currentPosition, SEEK_SET); //再將文件指針復(fù)位
if (length > PCM_SIZE * 2 * sizeof(short int)) {
read = (int)fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
if (read == 0) write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
else write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
NSLog(@"轉(zhuǎn)碼中...start:%ld,end:%ld",startPosition, endPosition);
}else {
[NSThread sleepForTimeInterval:0.2];
}
} while (self.isRecording);
lame_close(lame);
fclose(mp3);
fclose(pcm);
}
@catch (NSException *exception) {
NSLog(@"%@",[exception description]);
dispatch_async(dispatch_get_main_queue(), ^{
// do something
});
}
@finally {
dispatch_async(dispatch_get_main_queue(), ^{
// do something
});
}
}
存儲部分就跳過了
影響正常使用錄音功能的因素
-
當(dāng)app處于后臺時
設(shè)置AVAudioSession
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord /*withOptions: AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers*/ error:&error];
-
錄音的同時栋豫,需要使用揚聲器
設(shè)置AVAudioSession
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions: AVAudioSessionCategoryOptionDefaultToSpeaker/* | AVAudioSessionCategoryOptionMixWithOthers*/ error:&error];
-
錄音的同時,其他app調(diào)用了揚聲器或者麥克風(fēng)
設(shè)置AVAudioSession
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions: AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers error:&error];
-
錄音的時候有電話打進(jìn)來或者其他可能導(dǎo)致錄音中斷的情況
監(jiān)聽AVAudioSessionInterruptionNotification
- (instancetype)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionInterruptionNotification:) name:AVAudioSessionInterruptionNotification object:nil];
[self addCallMonitor];
}
return self;
}
//audio被打斷
- (void)audioSessionInterruptionNotification:(NSNotification *)notifi {
if (self.orderId == nil) { return ; }
NSInteger type = [notifi.userInfo[AVAudioSessionInterruptionTypeKey] integerValue];
switch (type) {
case AVAudioSessionInterruptionTypeBegan: { //打斷時暫停錄音
//do something
}break;
case AVAudioSessionInterruptionTypeEnded: {//結(jié)束后開啟錄音
NSInteger option = [notifi.userInfo[AVAudioSessionInterruptionOptionKey] integerValue];
if (option == AVAudioSessionInterruptionOptionShouldResume) {//需要重啟則重啟
//do something
}else {
//do something
}
}break;
};
NSLog(@"%@", notifi);
}
-
在打電話的時候谚殊,調(diào)用了麥克風(fēng)
由于在通話中調(diào)用麥克風(fēng)會失敗丧鸯,所以需要監(jiān)聽通話結(jié)束時,恢復(fù)錄音功能
//添加電話監(jiān)聽
- (void)addCallMonitor {
if (@available(iOS 10.0, *)) {
self.callObserver = [[CXCallObserver alloc] init];
[self.callObserver setDelegate:self queue:dispatch_get_main_queue()];
} else {
self.callCenter = [[CTCallCenter alloc] init];
[self.callCenter setCallEventHandler:^(CTCall * call) {
if ([call.callState isEqualToString:CTCallStateDisconnected]) {//通話中斷后嫩絮,重新開啟錄音
//do something
}
}];
}
}
//通話中斷后丛肢,重新開啟錄音
- (void)callObserver:(CXCallObserver *)callObserver callChanged:(CXCall *)call API_AVAILABLE(ios(10.0)) {
if (call.hasEnded) {
//do something
}
}
總結(jié)
上述介紹,只是羅列了一部分錄音的操作剿干,具體的實現(xiàn)還要根據(jù)業(yè)務(wù)不同蜂怎,添加其他的邏輯,若有不明之處置尔,可以評論溝通杠步。不完善之處,還請慷慨指出榜轿。