iOS錄音模組實踐[AVAudioRecoder]

littleplayer發表於2018-04-01

題記

這個模組是我們兒童專案的一個小功能,佔一個迭代版本需求的40%左右,開發週期12.5day,當然包含了整個IM邏輯,語音錄製1天。

實現方案分析

需求:IM語音聊天,IM採用騰訊的IM,語音錄製部分有自己的UI設計,大概是這個樣子(彈出一個pop框,先用合成器說一段“現在給xxx留言吧”,馬上開始錄製音訊,5秒後出現可以傳送按鈕,最大30s傳送語音,留言過程中要展示聲音波形圖)

方案:1.用AVAudioRecorder,高度封裝,之前專案已經實踐過,有無聲音分貝未知。 2.用AudioUnit實現,過於細粒度,要處理音訊buffer。

根據需求來說方案1看起比較貼近。

實踐

我就直接貼程式碼了,程式碼中會做註釋,其中有寫CallBack,許可權使用到的其他類就不上了,理解就好。

#import <Foundation/Foundation.h>

#import "CallBackInterface.h" //一個通用的CallBack宣告頭

@interface AudioRecorder : NSObject

@property(nonatomic, copy) CallBackTwo callBack;

@property(nonatomic, copy) CallBackOne canSendCallBack;

@property(nonatomic, assign) NSInteger volume;

@property(readonly, assign) BOOL isRecording;

- (BOOL)startRecord:(NSString *)storeFile;

- (void)cancelRecord;

- (void)endRecord;

- (void)resetEnvironment;

@end

複製程式碼

#import "AudioRecorder.h"

#import <ReactiveCocoa.h>
#import <AVFoundation/AVFoundation.h>

#import "UALogger.h"
#import "PermissionUtils.h" //一些手機許可權呼叫類,包括這次的micro許可權請求
#import "HWWeakTimer.h" //這個就不說了搜一下
#import "NSFileManager+Message.h" //錄音檔案管理


@interface AudioRecorder () <AVAudioRecorderDelegate>

@property(nonatomic, strong) NSString *storeFile;
@property(nonatomic, strong) AVAudioRecorder *recorder;

@property(nonatomic, strong) NSTimer *timer;
@property(nonatomic, assign) NSTimeInterval time; ///<  記錄當前錄音時間


@end


@implementation AudioRecorder

- (void)dealloc {
    [self endRecord];
}

- (BOOL)startRecord:(NSString *)storeFile {
    self.storeFile = storeFile;
    __block BOOL checkAuth = NO;
    __block BOOL success = NO;
    @weakify(self);
    //錄音許可權開啟
    [PermissionUtils openMic:^(BOOL auth) {
        @strongify(self);
        if (auth) {
            [self setActive];
            success = [self.recorder prepareToRecord];
            [self.recorder record];
            [self timer];
        }
        checkAuth = YES;
    }];
    //可以看看這種方式處理執行緒等待,這兒想起了一個故事,就略了
    while (!checkAuth) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.05]];
    }
    return success;
}

- (void)setActive {
    @try {
        NSError *error;
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&error];
        if (error) {
            UALog(@"[AudioRecorder][ERROR][StartRecord] %@", error);
        }
        error = nil;

        [[AVAudioSession sharedInstance] setActive:YES error:&error];
        if (error) {
            UALog(@"[AudioRecorder][ERROR][CallBack] %@", error);
        }

    } @catch (NSException *e) {

    }
}

- (void)cancelRecord {
    self.callBack = NULL;
    [self endRecord];
    _recorder.delegate = nil; // cancel 時快速釋放,避免引用造成的AVAudioSession Deactive失敗
    [self resetEnvironment];
}

- (void)endRecord {
    self.canSendCallBack = NULL;
    [self invalidateTimer];
    if (_recorder) {
        [self.recorder stop];
        [self deactivedAudioSession];
        _recorder = nil;
    }
}

- (void)deactivedAudioSession {
    NSError *error = nil;
    if (![[AVAudioSession sharedInstance] setActive:NO error:&error]) {
        NSLog(@"%@: AVAudioSession.setDeActive failed: %@\n", NSStringFromClass(self.class), error ? [error localizedDescription] : @"nil");
    }
}

- (void)resetEnvironment {
    //check doc path
    NSFileManager *fm = [NSFileManager defaultManager];
    if ([fm fileExistsAtPath:self.storeFile]) {
        NSError *error;
//        [fm removeItemAtPath:self.storeFile error:&error];
        if (error) {
            UALog(@"[AudioRecorder][FM]ERROR %@", error);
        }
    }
    self.time = 0.0f;
}


#pragma mark - AVAudioRecorderDelegate

- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag {
    if (self.callBack) {
        self.callBack(@(flag), nil);
    }
}

- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *__nullable)error {
    if (self.callBack) {
        self.callBack(@(NO), error);
    }
}

- (void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder {

}

- (void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder withOptions:(NSUInteger)flags {

}

- (void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder withFlags:(NSUInteger)flags {

}

- (void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder {

}


#pragma mark - getter

- (AVAudioRecorder *)recorder {
    if (!_recorder) {
        //錄音設定
        NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
        //設定錄音格式  AVFormatIDKey==kAudioFormatLinearPCM
        [recordSetting setValue:[NSNumber numberWithInt:kAudioFormatMPEG4AAC] forKey:AVFormatIDKey];
        //設定錄音取樣率(Hz) 如:AVSampleRateKey==8000/44100/96000(影響音訊的質量)
        [recordSetting setValue:[NSNumber numberWithFloat:44100] forKey:AVSampleRateKey];
        //錄音通道數  1 或 2
        [recordSetting setValue:[NSNumber numberWithInt:2] forKey:AVNumberOfChannelsKey];
        //線性取樣位數  8、16、24、32
        [recordSetting setValue:[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];
        //錄音的質量
        [recordSetting setValue:[NSNumber numberWithInt:AVAudioQualityHigh] forKey:AVEncoderAudioQualityKey];

        NSError *err;
        [self resetEnvironment];
        _recorder = [[AVAudioRecorder alloc] initWithURL:[NSURL fileURLWithPath:self.storeFile] settings:recordSetting error:&err];
        _recorder.delegate = self;
        _recorder.meteringEnabled = YES;
        if (err) {
            UALog(@"[AudioRecorder][Error] %@", err);
        }
    }
    return _recorder;
}

- (NSString *)storeFile {
    if (!_storeFile) {
        _storeFile = [[NSFileManager defaultManager] temRecordAudioFile];
    }
    return _storeFile;
}

- (BOOL)isRecording {
    return self.recorder.isRecording;
}

- (NSTimer *)timer {
    if (!_timer) {
        @weakify(self);
        _timer = [HWWeakTimer scheduledTimerWithTimeInterval:.05 block:^(id userInfo) {
            @strongify(self);
            self.time += .05;
            [self log];
            if (self.canSendCallBack) {
                self.canSendCallBack(@(self.time));
            }
            if (self.time >= 30.0f) {
                [self endRecord];
            }
        }                                           userInfo:nil repeats:YES];
    }
    return _timer;
}

- (void)log {
    [_recorder updateMeters];
    self.volume = (NSInteger) (pow(10, 0.05 * [_recorder peakPowerForChannel:0]) * 100);
}

- (void)invalidateTimer {
    if (!_timer) {
        [_timer invalidate];
        _timer = nil;
    }
}

@end
複製程式碼

上面說到,UI設計有一個音訊分貝的波形圖,開始寫的時候找到AVAudioRecoder有分貝最大值和平均值,

- (float)peakPowerForChannel:(NSUInteger)channelNumber;
- (float)averagePowerForChannel:(NSUInteger)channelNumber;

複製程式碼

但你何時取都是-160,讀文件發現構造recoder時候要設定 meteringEnabled = YES;

@property(getter=isMeteringEnabled) BOOL meteringEnabled;
複製程式碼

且在每次取分貝的時候要更新一次

- (void)updateMeters;
複製程式碼

轉化為 0~100的值作為波形圖的引數

 self.volume = (NSInteger) (pow(10, 0.05 * [_recorder peakPowerForChannel:0]) * 100);
複製程式碼

主意事項

1.AudioSession 持有和釋放 由於在語音錄製前有段語音合成“現在給xxx留言吧”,導致後面總是會報"AVAudioSession I/O"的一個錯誤,開始以為是各處AVAudioSession.active 與 setDeActive

[[AVAudioSession sharedInstance] setActive:YES error:&error];
與
[[AVAudioSession sharedInstance] setActive:NO error:&error
不成對造成的
複製程式碼

排查了播放器,語音合成播放器(百度和訊飛的),以及錄音模組成對釋放問題,最後驗證顯示呼叫還是沒用,最後分析合成器哪一塊釋放是不是耗時比較多,而我很快進入錄製,哪邊還沒有結束,然後造成我錄製一到兩秒,那邊的釋放訊號過來了,而這回AVAudioSession中確實錄製。 最後給合成器那邊給了0.5s處理釋放,延遲錄製0.5s解決此問題,由於合成器是第三方的,這種妥協是沒有辦法的。

2.AVRecorder崩潰問題 AVRecorder頁面邏輯我是Present一個ViewController上來,在點選周邊灰色層是取消錄製,但往往我們會很快的點選灰色層,又很快的點選錄製,周而復始崩潰。 這塊處理有個缺陷,我的cancel釋放有一個是放到

- dismissViewControllerAnimated:completion:
複製程式碼

的completion中,其實往往沒有時間處理釋放,所以模態出來的頁面的釋放不要把耗時的操作放到這塊。 但所謂的使用者暴力開始錄製取消錄製任然會出現崩潰,這塊只能上防止重複點選大法一個category奉上:

#import <Foundation/Foundation.h>

@interface NSObject (MultiClick)

@property (nonatomic, assign) NSTimeInterval acceptInterval; ///< 間隔 s
@property (nonatomic, assign) NSTimeInterval acceptTime;     ///< 上次觸發事件時間

/**
 * 是否接受本次事件
 */
- (BOOL)canAcceptEvent;

@end


#import "NSObject+MultiClick.h"
#import <objc/runtime.h>
#import "UALogger.h"

const char *NSObject_acceptTime = "NSObject_acceptTime";
const char *NSObject_acceptInterval = "NSObject_acceptInterval";

@implementation NSObject (MultiClick)

- (NSTimeInterval)acceptTime {
    return [objc_getAssociatedObject(self, NSObject_acceptTime) doubleValue];
}

- (void)setAcceptTime:(NSTimeInterval)acceptTime {
    objc_setAssociatedObject(self, NSObject_acceptTime, @(acceptTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSTimeInterval)acceptInterval {
    return [objc_getAssociatedObject(self, NSObject_acceptInterval) doubleValue];
}

- (void)setAcceptInterval:(NSTimeInterval)acceptInterval {
    objc_setAssociatedObject(self, NSObject_acceptInterval, @(acceptInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)canAcceptEvent {
    if (self.acceptInterval == 0) {
        return NO;
    }

    if ([[NSDate date] timeIntervalSince1970] - self.acceptTime > self.acceptInterval) {
        self.acceptTime = [[NSDate date] timeIntervalSince1970];
        return YES;
    }
    return NO;
}


@end
複製程式碼
使用方法
具體某一個類初始化的時候寫入防重點間隔
self.acceptInterval = 1;

事件方法第一行加
if (![self canAcceptEvent]) return;
複製程式碼

最後,感謝!感謝身邊的coder能答疑解惑,感謝合作partner很給力,so 能愉快的寫幾行程式碼還是挺愉快的。

相關文章