iOS實現(xiàn)錄音功能

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處于后臺時
    屏幕快照 2019-09-17 11.06.16.png

    設(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ù)不同蜂怎,添加其他的邏輯,若有不明之處置尔,可以評論溝通杠步。不完善之處,還請慷慨指出榜轿。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末幽歼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谬盐,更是在濱河造成了極大的恐慌甸私,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件飞傀,死亡現(xiàn)場離奇詭異颠蕴,居然都是意外死亡泣刹,警方通過查閱死者的電腦和手機(jī)助析,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門犀被,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人外冀,你說我怎么就攤上這事寡键。” “怎么了雪隧?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵西轩,是天一觀的道長。 經(jīng)常有香客問我脑沿,道長藕畔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任庄拇,我火速辦了婚禮注服,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘措近。我一直安慰自己溶弟,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布瞭郑。 她就那樣靜靜地躺著辜御,像睡著了一般。 火紅的嫁衣襯著肌膚如雪屈张。 梳的紋絲不亂的頭發(fā)上擒权,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機(jī)與錄音阁谆,去河邊找鬼碳抄。 笑死,一個胖子當(dāng)著我的面吹牛笛厦,可吹牛的內(nèi)容都是我干的纳鼎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼裳凸,長吁一口氣:“原來是場噩夢啊……” “哼贱鄙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起姨谷,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤逗宁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后梦湘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞎颗,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡件甥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了哼拔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片引有。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖倦逐,靈堂內(nèi)的尸體忽然破棺而出譬正,到底是詐尸還是另有隱情,我是刑警寧澤檬姥,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布曾我,位于F島的核電站,受9級特大地震影響健民,放射性物質(zhì)發(fā)生泄漏抒巢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一秉犹、第九天 我趴在偏房一處隱蔽的房頂上張望蛉谜。 院中可真熱鬧,春花似錦凤优、人聲如沸悦陋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俺驶。三九已至,卻和暖如春棍辕,著一層夾襖步出監(jiān)牢的瞬間暮现,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工楚昭, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留栖袋,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓抚太,卻偏偏與公主長得像塘幅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子尿贫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容