AVFoundation 播放和錄製音頻

1 音頻會話

1.1 分類 category

iOS 利用音頻會話(audio session)實現可管理的音頻環境,音頻會話提供簡單實用的方法使 OS 得知應用程序應該如何與 iOS 音頻環境進行交互。AVFoundation 定義了 7 種分類來描述音頻行爲數組

分類 做用 是否容許混音 音頻輸入輸出模式 是否支持後臺 是否遵循靜音切換
Ambient 遊戲、效率應用程序 支持 O 不支持 不支持
Solo Ambient(default) 遊戲、效率應用程序 不支持 O 不支持 遵循
Playback 音頻和視頻播放器 可選 O 支持 不遵循
Record 錄音機、音頻捕捉 不支持 I 支持 不遵循
Play and Record VoIP、語音聊天 可選 I/O 支持 不遵循
Audio Processing 離線會話和處理 F 不能播放和錄製 不遵循
Multi-Route 使用外部硬件的高級 A/V 應用程序 F I/O 不遵循

同時能夠用 options 和 modes 進一步自定義開發。網絡

1.1.1 options

options 有如下選項session

  • AVAudioSessionCategoryOptionMixWithOthers

支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute,AVAudioSessionCategoryAmbient 自動設置了此選項,AVAudioSessionCategoryOptionDuckOthers 和AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 也自動設置了此選項。若是使用這個選項激活會話,應用程序的音頻不會中斷從其餘應用程序(如音樂應用程序)的音頻,不然激活會話會打斷其餘音頻會話。less

  • AVAudioSessionCategoryOptionDuckOthers

支持 AVAudioSessionCategoryAmbient,AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute。設置此選項可以在播放音頻時低音量聽到後臺播放的其餘音頻。整個選項週期與會話激活週期一致。ide

  • AVAudioSessionCategoryOptionAllowBluetooth

支持 AVAudioSessionCategoryRecord,AVAudioSessionCategoryPlayAndRecord;容許藍牙免提設備啓用。當應用使用 setPreferredInput:error: 方法選擇了藍牙無線設備做爲輸入時,也會自動選擇相應的藍牙設備做爲輸出,使用 MPVolumeView 對象將藍牙設備做爲輸出時,輸入也會相應改變。oop

  • AVAudioSessionCategoryOptionDefaultToSpeaker

支持 AVAudioSessionCategoryPlayAndRecord;在沒有其餘的音頻路徑(如耳機)可使用的狀況下設置這個選項,會議音頻將經過設備的內置揚聲器播放。當不設置此選項,而且沒有其餘的音頻輸出可用或選擇時,音頻將經過接收器播放。只有 iPhone 設備都配備有一個接收器; iPad 和 iPod touch 設備,此選項沒有任何效果ui

當你的 iPhone 接有多個外接音頻設備時(耳塞,藍牙耳機等),AudioSession 將遵循 last-in wins 的原則來選擇外接設備,即聲音將被導向最後接入的設備。編碼

當沒有接入任何音頻設備時,通常狀況下聲音會默認從揚聲器出來,但有一個例外的狀況:在 PlayAndRecord 這個 category 下,聽筒會成爲默認的輸出設備。若是你想要改變這個行爲,能夠提供 MPVolumeView 來讓用戶切換到揚聲器,也可經過 overrideOutputAudioPort 方法來 programmingly 切換到揚聲器,也能夠修改 category option 爲AVAudioSessionCategoryOptionDefaultToSpeaker。url

  • AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers

支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute,設置此選項能使應用程序的音頻會話與其餘會話混合,可是會中斷使用了 AVAudioSessionModeSpokenAudio 模式的會話。其餘應用的音頻會在此會話啓動後暫停,並在此會話關閉後從新恢復。spa

在用到 AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 選項時,中斷了其餘應用的音頻後,本身的應用音頻結束播放時,若想恢復其餘應用的音頻,須要在關閉音頻會話的時候設置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 選項

[session setActive:NO
       withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
             error:<#Your error object, or nil for testing#>];
複製代碼
  • AVAudioSessionCategoryOptionAllowAirPlay

支持 AVAudioSessionCategoryPlayAndRecord,容許會話在 AirPlay 設備上執行。

1.1.2 mode

mode 用於定製化 audio sessions,若是將分類的 mode 設置不合理會執行默認的模式行爲,如將 AVAudioSessionCategoryMultiRoute 類別設置 AVAudioSessionModeGameChat 模式。

  • AVAudioSessionModeDefault 默認音頻會話模式

  • AVAudioSessionModeVoiceChat 若是應用須要執行例如 VoIP 類型的雙向語音通訊則選擇此模式

  • AVAudioSessionModeVideoChat 若是應用正在進行在線視頻會議,請指定此模式

  • AVAudioSessionModeGameChat 該模式由Game Kit 提供給使用 Game Kit 的語音聊天服務的應用程序設置

  • AVAudioSessionModeVideoRecording 若是應用正在錄製電影,則選此模式

  • AVAudioSessionModeMeasurement 若是您的應用正在執行音頻輸入或輸出的測量,請指定此模式

  • AVAudioSessionModeMoviePlayback 若是您的應用正在播放電影內容,請指定此模式

  • AVAudioSessionModeSpokenAudio 當須要持續播放語音,同時但願在其餘程序播放短語音時暫停播放此應用語音,選取此模式

1.2 配置音頻會話

首先得到指向 AVAudioSession 的單例指針,設置合適的分類,最後激活會話。

AVAudioSession *session = [AVAudioSession sharedInstance];

    NSError *error;
    if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
        NSLog(@"Category Error: %@", [error localizedDescription]);
    }

    if (![session setActive:YES error:&error]) {
        NSLog(@"Activation Error: %@", [error localizedDescription]);
    }
複製代碼

2. 播放音頻

AVAudioPlayer 構建於 Core Audio 的 C-based Audio Queue Services 最頂層,侷限性在於沒法從網絡流播放音頻,不能訪問原始音頻樣本,不能知足很是低的時延。

2.1 建立 AVAudioPlayer

能夠經過 NSData 或本地音頻文件的 NSURL 兩種方式建立 AVAudioPlayer。

NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"mp3"];
    self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
    if (self.player) {
        [self.player prepareToPlay];
    }
複製代碼

建立出 AVAudioPlayer 後建議調用 prepareToPlay 方法,這個方法會取得須要的音頻硬件並預加載 Audio Queue 的緩衝區,固然若是不主動調用,執行 play 方法時也會默認調用,可是會形成輕微播放的延時。

2.2 對播放進行控制

AVAudioPlayer 的 play 能夠播放音頻,stop 和 pause 均可以暫停播放,可是 stop 會撤銷調用 prepareToPlay 所作的設置。

  • 修改播放器的音量:播放器音量獨立於系統音量,音量或播放增益定義爲 0.0(靜音)到 1.0(最大音量)之間的浮點值
  • 修改播放器的 pan 值:容許使用立體聲播放聲音,pan 值從 -1.0(極左)到 1.0(極右),默認值 0.0(居中)
  • 調整播放率:0.5(半速)到 2.0(2 倍速)
  • 設置 numberOfLoops 實現無縫循環:-1 表示無限循環(音頻循環能夠是未壓縮的線性 PCM 音頻,也能夠是 AAC 之類的壓縮格式音頻,MP3 格式不推薦循環)
  • 音頻計量:當播放發生時從播放器讀取音量力度的平均值和峯值

2.3 實踐

2.3.1 播放音頻

NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
        for (AVAudioPlayer *player in self.players) {
            [player playAtTime:delayTime];
        }
        self.playing = YES;
複製代碼

對於多個須要播放的音頻,若是但願同步播放效果,則須要捕捉當前設備時間並添加一個小延時,從而具備一個從開始播放時間計算的參照時間。deviveCurrentTime 是一個獨立於系統事件的音頻設備的時間值,當有多於 audioPlayer 處於 play 或者 pause 狀態時 deviveCurrentTime 會單調增長,沒有時置位爲 0。playAtTime 的參數 time 要求必須是基於 deviveCurrentTime 且大於等於 deviveCurrentTime 的時間。

2.3.2 暫停播放

for (AVAudioPlayer *player in self.players) {
            [player stop];
            player.currentTime = 0.0f;
        }
複製代碼

暫停時須要將 audioPlayer 的 currentTime 值設置爲 0.0,當音頻正在播放時,這個值用於標識當前播放位置的偏移,不播放音頻時標識從新播放音頻的起始偏移。

2.3.4 修改音量、pan值、播放速率和循環

player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;
複製代碼

2.4 配置音頻會話

若是但願應用程序播放音頻時屏蔽靜音切換動做,須要設置會話分類爲 AVAudioSessionCategoryPlayback,可是若是但願按下鎖屏後還能夠播放,就須要在 plist 里加入一個 Required background modes 類型的數組,在其中添加 App plays audio or streams audio/video using AirPlay。

2.5 處理中斷事件

中斷事件是指電話呼入、鬧鐘響起、彈出 FaceTime 等,中斷事件發生時系統會調用 AVAudioPlayer 的 AVAudioPlayerDelegate 類型的 delegate 的下列方法

- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 8_0);
- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0);
複製代碼

中斷結束調用的方法會帶入一個 options 參數,若是是 AVAudioSessionInterruptionOptionShouldResume 則代表能夠恢復播放音頻了。

2.6 處理線路改變

在 iOS 設備上添加或移除音頻輸入、輸出線路時會引起線路改變,最佳實踐是,插入耳機時播放動做不改動,拔出耳機時應當暫停播放。

首先須要監聽通知

NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
        [nsnc addObserver:self
                 selector:@selector(handleRouteChange:)
                     name:AVAudioSessionRouteChangeNotification
                   object:[AVAudioSession sharedInstance]];
複製代碼

而後判斷是舊設備不可達事件,進一步取出舊設備的描述,判斷舊設備是不是耳機,再作暫停播放處理。

- (void)handleRouteChange:(NSNotification *)notification {

    NSDictionary *info = notification.userInfo;

    AVAudioSessionRouteChangeReason reason =
        [info[AVAudioSessionRouteChangeReasonKey] unsignedIntValue];

    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {

        AVAudioSessionRouteDescription *previousRoute =
            info[AVAudioSessionRouteChangePreviousRouteKey];

        AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
        NSString *portType = previousOutput.portType;

        if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
            [self stop];
            [self.delegate playbackStopped];
        }
    }
}
複製代碼

這裏 AVAudioSessionPortHeadphones 只包含了有線耳機,無線藍牙耳機須要判斷 AVAudioSessionPortBluetoothA2DP 值。

3. 錄製音頻

AVAudioRecorder 用於負責錄製音頻。

3.1 建立 AVAudioRecorder

建立 AVAudioRecorder 須要如下信息

  • 用於寫入音頻的本地文件 URL
  • 用於配置錄音會話鍵值信息的字典
  • 用於捕捉錯誤的 NSError
NSString *tmpDir = NSTemporaryDirectory();
        NSString *filePath = [tmpDir stringByAppendingPathComponent:@"memo.caf"];
        NSURL *fileURL = [NSURL fileURLWithPath:filePath];

        NSDictionary *settings = @{
                                   AVFormatIDKey : @(kAudioFormatAppleIMA4),
                                   AVSampleRateKey : @44100.0f,
                                   AVNumberOfChannelsKey : @1,
                                   AVEncoderBitDepthHintKey : @16,
                                   AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                                   };

        NSError *error;
        self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:&error];
        if (self.recorder) {
            self.recorder.delegate = self;
            self.recorder.meteringEnabled = YES;
            [self.recorder prepareToRecord];
        } else {
            NSLog(@"Error: %@", [error localizedDescription]);
        }
複製代碼

prepareToRecord 方法執行底層 Audio Queue 初始化必要過程,並在指定位置建立文件。

3.2 通用設置參數

  • 音頻格式

AVFormatIDKey 鍵對應寫入內容的音頻格式,它有如下可選值

kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioFormatAppleLossless
kAudioFormatAppleIMA4
kAudioFormatiLBC
kAudioFormatULaw
複製代碼

kAudioFormatLinearPCM 會將未壓縮的音頻流寫入文件,文件體積大。kAudioFormatMPEG4AAC 和 kAudioFormatAppleIMA4 的壓縮格式會顯著縮小文件,並保證高質量音頻內容。可是要注意,制定的音頻格式與文件類型應該兼容,例如 wav 格式對應 kAudioFormatLinearPCM 值。

  • 採樣率

AVSampleRateKey 指示採樣率,即對輸入的模擬音頻信號每一秒內的採樣數。經常使用值 8000,16000,22050,44100。

  • 通道數

AVNumberOfChannelsKey 指示定義記錄音頻內容的通道數,除非使用外部硬件錄製,不然一般選擇單聲道。

  • 編碼位元深度

AVEncoderBitDepthHintKey 指示編碼位元深度,從 8 到 32。

  • 音頻質量

AVEncoderAudioQualityKey 指示音頻質量,可選值有 AVAudioQualityMin, AVAudioQualityLow, AVAudioQualityMedium, AVAudioQualityHigh, AVAudioQualityMax。

3.3 實踐

3.3.1 配置音頻會話

錄音和播放應用應當使用 AVAudioSessionCategoryPlayAndRecord 分類來配置會話。

AVAudioSession *session = [AVAudioSession sharedInstance];

    NSError *error;
    if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
        NSLog(@"Category Error: %@", [error localizedDescription]);
    }

    if (![session setActive:YES error:&error]) {
        NSLog(@"Activation Error: %@", [error localizedDescription]);
    }
複製代碼

注意錄音前須要申請麥克風權限。

3.3.2 錄音控制

對錄音過程的控制以下

[self.recorder record];
[self.recorder pause];
[self.recorder stop];
複製代碼

其中選擇了 stop 錄音即中止,此時 AVAudioRecorder 會調用其遵循 AVAudioRecorderDelegate 協議的代理的 - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag 方法。

3.3.3 錄音保存

在初始化 AVAudioRecorder 時指定了臨時文件目錄做爲存儲音頻的位置,音頻錄製結束時須要保存到 Document 目錄下

NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate];
    NSString *filename = [NSString stringWithFormat:@"%@-%f.m4a", name, timestamp];

    NSString *docsDir = [self documentsDirectory];
    NSString *destPath = [docsDir stringByAppendingPathComponent:filename];

    NSURL *srcURL = self.recorder.url;
    NSURL *destURL = [NSURL fileURLWithPath:destPath];

    NSError *error;
    BOOL success = [[NSFileManager defaultManager] copyItemAtURL:srcURL toURL:destURL error:&error];
    if (success) {
        handler(YES, [THMemo memoWithTitle:name url:destURL]);
        [self.recorder prepareToRecord];
    } else {
        handler(NO, error);
    }
複製代碼

這裏調用了 NSFileManager 的 copyItemAtURL 方法將文件內容拷貝到 Document 目錄下。

3.3.4 展現時間

記錄音頻時須要展現時間提示用戶當前錄製時間,AVAudioRecorder 的 currentTime 屬性能夠獲知當前時間,將其格式化後便可進行展現

- (NSString *)formattedCurrentTime {
    NSUInteger time = (NSUInteger)self.recorder.currentTime;
    NSInteger hours = (time / 3600);
    NSInteger minutes = (time / 60) % 60;
    NSInteger seconds = time % 60;

    NSString *format = @"%02i:%02i:%02i";
    return [NSString stringWithFormat:format, hours, minutes, seconds];
}
複製代碼

可是須要實時展現時間的話,不能經過 KVO 來解決,只能加入到 NSTimer 中,每 0.5s 執行一次。

[self.timer invalidate];
    self.timer = [NSTimer timerWithTimeInterval:0.5
                                         target:self
                                       selector:@selector(updateTimeDisplay)
                                       userInfo:nil
                                        repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
複製代碼

3.3.5 可視化音頻信號

AVAudioRecorder 和 AVAudioPlayer 都有兩個方法獲取當前音頻的平均分貝和峯值分貝數據。

- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */
複製代碼

返回值從 -160dB(靜音) 到 0dB(最大分貝)。

獲取值以前要在初始化播放器或記錄器時設置 meteringEnabled 爲 YES。

首先須要將 -160 到 0 的分貝值轉爲 0 到 1 範圍內,須要用到下面這個類

@implementation THMeterTable {
    float _scaleFactor;
    NSMutableArray *_meterTable;
}

- (id)init {
    self = [super init];
    if (self) {
        float dbResolution = MIN_DB / (TABLE_SIZE - 1);

        _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
        _scaleFactor = 1.0f / dbResolution;

        float minAmp = dbToAmp(MIN_DB);
        float ampRange = 1.0 - minAmp;
        float invAmpRange = 1.0 / ampRange;

        for (int i = 0; i < TABLE_SIZE; i++) {
            float decibels = i * dbResolution;
            float amp = dbToAmp(decibels);
            float adjAmp = (amp - minAmp) * invAmpRange;
            _meterTable[i] = @(adjAmp);
        }
    }
    return self;
}

float dbToAmp(float dB) {
    return powf(10.0f, 0.05f * dB);
}

- (float)valueForPower:(float)power {
    if (power < MIN_DB) {
        return 0.0f;
    } else if (power >= 0.0f) {
        return 1.0f;
    } else {
        int index = (int) (power * _scaleFactor);
        return [_meterTable[index] floatValue];
    }
}

@end
複製代碼

接下來能夠實時獲取到分貝平均值和峯值

- (THLevelPair *)levels {
    [self.recorder updateMeters];
    float avgPower = [self.recorder averagePowerForChannel:0];
    float peakPower = [self.recorder peakPowerForChannel:0];
    float linearLevel = [self.meterTable valueForPower:avgPower];
    float linearPeak = [self.meterTable valueForPower:peakPower];
    return [THLevelPair levelsWithLevel:linearLevel peakLevel:linearPeak];
}
複製代碼

能夠看到獲取峯值和均值前必須調用 updateMeters 方法。

相關文章
相關標籤/搜索