iOS AVDemo(1):音頻采集,免費(fèi)獲取全部源碼丨音視頻工程示例

vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時(shí)獲得最新的音視頻技術(shù)文章忆矛。

畢加索《德拉加萊特紅磨坊》像素版

這個(gè)公眾號會路線圖 式的遍歷分享音視頻技術(shù)音視頻基礎(chǔ)(完成)音視頻工具(完成)音視頻工程示例(進(jìn)行中) → 音視頻工業(yè)實(shí)戰(zhàn)(準(zhǔn)備)舌剂。

iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā),最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后聊浅,再借助本地平臺的音視頻能力上手去實(shí)踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染過程餐抢,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(shù)據(jù)。

音視頻工程示例這個(gè)欄目低匙,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實(shí)現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)旷痕。

這里是第一篇:iOS 音頻采集 Demo。這個(gè) Demo 里包含以下內(nèi)容:

  • 1)實(shí)現(xiàn)一個(gè)音頻采集模塊顽冶;
  • 2)實(shí)現(xiàn)音頻采集邏輯并將采集的音頻存儲為 PCM 數(shù)據(jù)欺抗;
  • 3)詳盡的代碼注釋,幫你理解代碼邏輯和原理强重。

你可以在關(guān)注微信公眾號后绞呈,在公眾號發(fā)送消息『AVDemo』來獲取 Demo 的全部源碼。

1间景、音頻采集模塊

首先佃声,實(shí)現(xiàn)一個(gè) KFAudioConfig 類用于定義音頻采集參數(shù)的配置。這里包括了:采樣率倘要、量化位深圾亏、聲道數(shù)這幾個(gè)參數(shù)。這幾個(gè)參數(shù)的含義在前面介紹聲音基礎(chǔ)的文章《聲音的表示(3):聲音的數(shù)字化》中有過介紹碗誉。

KFAudioConfig.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface KFAudioConfig : NSObject
+ (instancetype)defaultConfig;

@property (nonatomic, assign) NSUInteger channels; // 聲道數(shù)召嘶,default: 2。
@property (nonatomic, assign) NSUInteger sampleRate; // 采樣率哮缺,default: 44100弄跌。
@property (nonatomic, assign) NSUInteger bitDepth; // 量化位深,default: 16尝苇。
@end

NS_ASSUME_NONNULL_END

KFAudioConfig.m

#import "KFAudioConfig.h"

@implementation KFAudioConfig

+ (instancetype)defaultConfig {
    KFAudioConfig *config = [[self alloc] init];
    config.channels = 2;
    config.sampleRate = 44100;
    config.bitDepth = 16;
    
    return config;
}

@end

接下來铛只,我們實(shí)現(xiàn)一個(gè) KFAudioCapture 類來實(shí)現(xiàn)音頻采集。

KFAudioCapture.h

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import "KFAudioConfig.h"

NS_ASSUME_NONNULL_BEGIN

@interface KFAudioCapture : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFAudioConfig *)config;

@property (nonatomic, strong, readonly) KFAudioConfig *config;
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音頻采集數(shù)據(jù)回調(diào)糠溜。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音頻采集錯(cuò)誤回調(diào)淳玩。

- (void)startRunning; // 開始采集音頻數(shù)據(jù)。
- (void)stopRunning; // 停止采集音頻數(shù)據(jù)非竿。
@end

NS_ASSUME_NONNULL_END

上面是 KFAudioCapture 的接口設(shè)計(jì)蜕着,可以看到這里除了初始化方法,主要是有獲取音頻配置以及音頻采集數(shù)據(jù)回調(diào)錯(cuò)誤回調(diào)的接口,另外就是開始采集停止采集的接口承匣。

在上面的音頻采集數(shù)據(jù)回調(diào)接口中蓖乘,我們返回的是 CMSampleBufferRef[1] 這個(gè)數(shù)據(jù)結(jié)構(gòu),這里我們重點(diǎn)介紹一下韧骗。官方文檔對 CMSampleBufferRef 描述如下:

A reference to a CMSampleBuffer. A CMSampleBuffer is a Core Foundation object containing zero or more compressed (or uncompressed) samples of a particular media type (audio, video, muxed, and so on).

CMSampleBufferRef 是對 CMSampleBuffer[2] 的一個(gè)引用嘉抒。所里這里核心的數(shù)據(jù)結(jié)構(gòu)是 CMSampleBuffer,關(guān)于它有如下幾點(diǎn)需要注意:

  • CMSampleBuffer 則是一個(gè) Core Foundation 的對象袍暴,這意味著它的接口是 C 語言實(shí)現(xiàn)些侍,它的內(nèi)存管理是非 ARC 的,需要手動管理政模,它與 Foundation 對象之間需要進(jìn)行橋接轉(zhuǎn)換岗宣。
  • CMSampleBuffer 是系統(tǒng)用來在音視頻處理的 pipeline 中使用和傳遞媒體采樣數(shù)據(jù)的核心數(shù)據(jù)結(jié)構(gòu)。你可以認(rèn)為它是 iOS 音視頻處理 pipeline 中的流通貨幣览徒,攝像頭采集的視頻數(shù)據(jù)接口狈定、麥克風(fēng)采集的音頻數(shù)據(jù)接口、編碼和解碼數(shù)據(jù)接口习蓬、讀取和存儲視頻接口纽什、視頻渲染接口等等,都以它作為參數(shù)躲叼。
  • CMSampleBuffer 中包含著零個(gè)或多個(gè)某一類型(audio芦缰、video、muxed 等)的采樣數(shù)據(jù)枫慷。比如:
    • 要么是一個(gè)或多個(gè)媒體采樣的 CMBlockBuffer[3]让蕾。其中可以封裝:音頻采集后、編碼后或听、解碼后的數(shù)據(jù)(如:PCM 數(shù)據(jù)探孝、AAC 數(shù)據(jù));視頻編碼后的數(shù)據(jù)(如:H.264 數(shù)據(jù))誉裆。
    • 要么是一個(gè) CVImageBuffer[4](也作 CVPixelBuffer[5])顿颅。其中包含媒體流中 CMSampleBuffers 的格式描述、每個(gè)采樣的寬高和時(shí)序信息足丢、緩沖級別和采樣級別的附屬信息粱腻。緩沖級別的附屬信息是指緩沖區(qū)整體的信息,比如播放速度斩跌、對后續(xù)緩沖數(shù)據(jù)的操作等绍些。采樣級別的附屬信息是指單個(gè)采樣的信息,比如視頻幀的時(shí)間戳耀鸦、是否關(guān)鍵幀等柬批。其中可以封裝:視頻采集后、解碼后等未經(jīng)編碼的數(shù)據(jù)(如:YCbCr 數(shù)據(jù)、RGBA 數(shù)據(jù))氮帐。

所以锻霎,了解完這些,就知道上面的音頻采集數(shù)據(jù)回調(diào)接口為什么會返回 CMSampleBufferRef 這個(gè)數(shù)據(jù)結(jié)構(gòu)了揪漩。因?yàn)樗ㄓ茫瑫r(shí)我們也可以從里面獲取到我們想要的 PCM 數(shù)據(jù)吏口。

#import "KFAudioCapture.h"
#import <AVFoundation/AVFoundation.h>
#import <mach/mach_time.h>

@interface KFAudioCapture ()
@property (nonatomic, assign) AudioComponentInstance audioCaptureInstance; // 音頻采集實(shí)例奄容。
@property (nonatomic, assign) AudioStreamBasicDescription audioFormat; // 音頻采集參數(shù)。
@property (nonatomic, strong, readwrite) KFAudioConfig *config;
@property (nonatomic, strong) dispatch_queue_t captureQueue;
@property (nonatomic, assign) BOOL isError;
@end

@implementation KFAudioCapture

#pragma mark - Lifecycle
- (instancetype)initWithConfig:(KFAudioConfig *)config {
    self = [super init];
    if (self) {
        _config = config;
        _captureQueue = dispatch_queue_create("com.KeyFrameKit.audioCapture", DISPATCH_QUEUE_SERIAL);
    }
    
    return self;
}
- (void)dealloc {
    // 清理音頻采集實(shí)例产徊。
    if (_audioCaptureInstance) {
        AudioOutputUnitStop(_audioCaptureInstance);
        AudioComponentInstanceDispose(_audioCaptureInstance);
        _audioCaptureInstance = nil;
    }
}

#pragma mark - Action
- (void)startRunning {
    if (self.isError) {
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(_captureQueue, ^{
        if (!weakSelf.audioCaptureInstance) {
            NSError *error = nil;
            // 第一次 startRunning 時(shí)創(chuàng)建音頻采集實(shí)例昂勒。
            [weakSelf setupAudioCaptureInstance:&error];
            if (error) {
                // 捕捉并回調(diào)創(chuàng)建音頻實(shí)例時(shí)的錯(cuò)誤。
                [weakSelf callBackError:error];
                return;
            }
        }
        
        // 開始采集舟铜。
        OSStatus startStatus = AudioOutputUnitStart(weakSelf.audioCaptureInstance);
        if (startStatus != noErr) {
            // 捕捉并回調(diào)開始采集時(shí)的錯(cuò)誤戈盈。
            [weakSelf callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioCapture class]) code:startStatus userInfo:nil]];
        }
    });
}

- (void)stopRunning {
    if (self.isError) {
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(_captureQueue, ^{
        if (weakSelf.audioCaptureInstance) {
            // 停止采集。
            OSStatus stopStatus = AudioOutputUnitStop(weakSelf.audioCaptureInstance);
            if (stopStatus != noErr) {
                // 捕捉并回調(diào)停止采集時(shí)的錯(cuò)誤谆刨。
                [weakSelf callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioCapture class]) code:stopStatus userInfo:nil]];
            }
        }
    });
}

#pragma mark - Utility
- (void)setupAudioCaptureInstance:(NSError **)error {
    // 1塘娶、設(shè)置音頻組件描述。
    AudioComponentDescription acd = {
        .componentType = kAudioUnitType_Output,
        //.componentSubType = kAudioUnitSubType_VoiceProcessingIO, // 回聲消除模式
        .componentSubType = kAudioUnitSubType_RemoteIO,
        .componentManufacturer = kAudioUnitManufacturer_Apple,
        .componentFlags = 0,
        .componentFlagsMask = 0,
    };
    
    // 2痊夭、查找符合指定描述的音頻組件刁岸。
    AudioComponent component = AudioComponentFindNext(NULL, &acd);
    
    // 3、創(chuàng)建音頻組件實(shí)例她我。
    OSStatus status = AudioComponentInstanceNew(component, &_audioCaptureInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
        
    // 4虹曙、設(shè)置實(shí)例的屬性:可讀寫。0 不可讀寫番舆,1 可讀寫酝碳。
    UInt32 flagOne = 1;
    AudioUnitSetProperty(_audioCaptureInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flagOne, sizeof(flagOne));
    
    // 5、設(shè)置實(shí)例的屬性:音頻參數(shù)恨狈,如:數(shù)據(jù)格式疏哗、聲道數(shù)、采樣位深拴事、采樣率等沃斤。
    AudioStreamBasicDescription asbd = {0};
    asbd.mFormatID = kAudioFormatLinearPCM; // 原始數(shù)據(jù)為 PCM,采用聲道交錯(cuò)格式刃宵。
    asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
    asbd.mChannelsPerFrame = (UInt32) self.config.channels; // 每幀的聲道數(shù)
    asbd.mFramesPerPacket = 1; // 每個(gè)數(shù)據(jù)包幀數(shù)
    asbd.mBitsPerChannel = (UInt32) self.config.bitDepth; // 采樣位深
    asbd.mBytesPerFrame = asbd.mChannelsPerFrame * asbd.mBitsPerChannel / 8; // 每幀字節(jié)數(shù) (byte = bit / 8)
    asbd.mBytesPerPacket = asbd.mFramesPerPacket * asbd.mBytesPerFrame; // 每個(gè)包的字節(jié)數(shù)
    asbd.mSampleRate = self.config.sampleRate; // 采樣率
    self.audioFormat = asbd;
    status = AudioUnitSetProperty(_audioCaptureInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 6衡瓶、設(shè)置實(shí)例的屬性:數(shù)據(jù)回調(diào)函數(shù)。
    AURenderCallbackStruct cb;
    cb.inputProcRefCon = (__bridge void *) self;
    cb.inputProc = audioBufferCallBack;
    status = AudioUnitSetProperty(_audioCaptureInstance, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &cb, sizeof(cb));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 7牲证、初始化實(shí)例哮针。
    status = AudioUnitInitialize(_audioCaptureInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
}

- (void)callBackError:(NSError *)error {
    self.isError = YES;
    if (error && self.errorCallBack) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.errorCallBack(error);
        });
    }
}

+ (CMSampleBufferRef)sampleBufferFromAudioBufferList:(AudioBufferList)buffers inTimeStamp:(const AudioTimeStamp *)inTimeStamp inNumberFrames:(UInt32)inNumberFrames description:(AudioStreamBasicDescription)description {
    CMSampleBufferRef sampleBuffer = NULL; // 待生成的 CMSampleBuffer 實(shí)例的引用。
    
    // 1、創(chuàng)建音頻流的格式描述信息十厢。
    CMFormatDescriptionRef format = NULL;
    OSStatus status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &description, 0, NULL, 0, NULL, NULL, &format);
    if (status != noErr) {
        CFRelease(format);
        return nil;
    }
    
    // 2等太、處理音頻幀的時(shí)間戳信息。
    mach_timebase_info_data_t info = {0, 0};
    mach_timebase_info(&info);
    uint64_t time = inTimeStamp->mHostTime;
    // 轉(zhuǎn)換為納秒蛮放。
    time *= info.numer;
    time /= info.denom;
    // PTS缩抡。
    CMTime presentationTime = CMTimeMake(time, 1000000000.0f);
    // 對于音頻,PTS 和 DTS 是一樣的包颁。
    CMSampleTimingInfo timing = {CMTimeMake(1, description.mSampleRate), presentationTime, presentationTime};
    
    // 3瞻想、創(chuàng)建 CMSampleBuffer 實(shí)例。
    status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, format, (CMItemCount) inNumberFrames, 1, &timing, 0, NULL, &sampleBuffer);
    if (status != noErr) {
        CFRelease(format);
        return nil;
    }
    
    // 4娩嚼、創(chuàng)建 CMBlockBuffer 實(shí)例蘑险。其中數(shù)據(jù)拷貝自 AudioBufferList,并將 CMBlockBuffer 實(shí)例關(guān)聯(lián)到 CMSampleBuffer 實(shí)例岳悟。
    status = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer, kCFAllocatorDefault, kCFAllocatorDefault, 0, &buffers);
    if (status != noErr) {
        CFRelease(format);
        return nil;
    }
    
    CFRelease(format);
    return sampleBuffer;
}

#pragma mark - Capture CallBack
static OSStatus audioBufferCallBack(void *inRefCon,
                                    AudioUnitRenderActionFlags *ioActionFlags,
                                    const AudioTimeStamp *inTimeStamp,
                                    UInt32 inBusNumber,
                                    UInt32 inNumberFrames,
                                    AudioBufferList *ioData) {
    @autoreleasepool {
        KFAudioCapture *capture = (__bridge KFAudioCapture *) inRefCon;
        if (!capture) {
            return -1;
        }
        
        // 1佃迄、創(chuàng)建 AudioBufferList 空間,用來接收采集回來的數(shù)據(jù)贵少。
        AudioBuffer buffer;
        buffer.mData = NULL;
        buffer.mDataByteSize = 0;
        // 采集的時(shí)候設(shè)置了數(shù)據(jù)格式是 kAudioFormatLinearPCM呵俏,即聲道交錯(cuò)格式,所以即使是雙聲道這里也設(shè)置 mNumberChannels 為 1春瞬。
        // 對于雙聲道的數(shù)據(jù)柴信,會按照采樣位深 16 bit 每組,一組接一組地進(jìn)行兩個(gè)聲道數(shù)據(jù)的交錯(cuò)拼裝宽气。
        buffer.mNumberChannels = 1;
        AudioBufferList buffers;
        buffers.mNumberBuffers = 1;
        buffers.mBuffers[0] = buffer;
        
        // 2随常、獲取音頻 PCM 數(shù)據(jù),存儲到 AudioBufferList 中萄涯。
        // 這里有幾個(gè)問題要說明清楚:
        // 1)每次回調(diào)會過來多少數(shù)據(jù)绪氛?
        // 按照上面采集音頻參數(shù)的設(shè)置:PCM 為聲道交錯(cuò)格式、每幀的聲道數(shù)為 2涝影、采樣位深為 16 bit枣察。這樣每幀的字節(jié)數(shù)是 4 字節(jié)(左右聲道各 2 字節(jié))。
        // 返回?cái)?shù)據(jù)的幀數(shù)是 inNumberFrames燃逻。這樣一次回調(diào)回來的數(shù)據(jù)字節(jié)數(shù)是多少就是:mBytesPerFrame(4) * inNumberFrames序目。
        // 2)這個(gè)數(shù)據(jù)回調(diào)的頻率跟音頻采樣率有關(guān)系嗎?
        // 這個(gè)數(shù)據(jù)回調(diào)的頻率與音頻采樣率(上面設(shè)置的 mSampleRate 44100)是沒關(guān)系的伯襟。聲道數(shù)猿涨、采樣位深、采樣率共同決定了設(shè)備單位時(shí)間里采樣數(shù)據(jù)的大小姆怪,這些數(shù)據(jù)是會緩沖起來叛赚,然后一塊一塊的通過這個(gè)數(shù)據(jù)回調(diào)給我們澡绩,這個(gè)回調(diào)的頻率是底層一塊一塊給我們數(shù)據(jù)的速度,跟采樣率無關(guān)俺附。
        // 3)這個(gè)數(shù)據(jù)回調(diào)的頻率是多少肥卡?
        // 這個(gè)數(shù)據(jù)回調(diào)的間隔是 [AVAudioSession sharedInstance].preferredIOBufferDuration,頻率即該值的倒數(shù)事镣。我們可以通過 [[AVAudioSession sharedInstance] setPreferredIOBufferDuration:1 error:nil] 設(shè)置這個(gè)值來控制回調(diào)頻率步鉴。
        OSStatus status = AudioUnitRender(capture.audioCaptureInstance,
                                          ioActionFlags,
                                          inTimeStamp,
                                          inBusNumber,
                                          inNumberFrames,
                                          &buffers);
        
        // 3、數(shù)據(jù)封裝及回調(diào)璃哟。
        if (status == noErr) {
            // 使用工具方法將數(shù)據(jù)封裝為 CMSampleBuffer唠叛。
            CMSampleBufferRef sampleBuffer = [KFAudioCapture sampleBufferFromAudioBufferList:buffers inTimeStamp:inTimeStamp inNumberFrames:inNumberFrames description:capture.audioFormat];
            // 回調(diào)數(shù)據(jù)。
            if (capture.sampleBufferOutputCallBack) {
                capture.sampleBufferOutputCallBack(sampleBuffer);
            }
            if (sampleBuffer) {
                CFRelease(sampleBuffer);
            }
        }
        
        return status;
    }
}

@end

上面是 KFAudioCapture 的實(shí)現(xiàn)沮稚,從代碼上可以看到主要有這幾個(gè)部分:

  • 1)創(chuàng)建音頻采集實(shí)例。第一次調(diào)用 -startRunning 才會創(chuàng)建音頻采集實(shí)例册舞。
    • -setupAudioCaptureInstance: 方法中實(shí)現(xiàn)蕴掏。
  • 2)處理音頻采集實(shí)例的數(shù)據(jù)回調(diào),并在回調(diào)中將數(shù)據(jù)封裝到 CMSampleBufferRef 結(jié)構(gòu)中调鲸,拋給 KFAudioCapture 的對外數(shù)據(jù)回調(diào)接口盛杰。
    • audioBufferCallBack(...) 方法中實(shí)現(xiàn)回調(diào)處理邏輯。
    • 其中封裝 CMSampleBufferRef 用到了 +sampleBufferFromAudioBufferList:inTimeStamp:inNumberFrames:description: 方法藐石。
  • 3)實(shí)現(xiàn)開始采集和停止采集邏輯即供。
    • 分別在 -startRunning-stopRunning 方法中實(shí)現(xiàn)。注意于微,這里是開始和停止操作都是放在串行隊(duì)列中通過 dispatch_async 異步處理的逗嫡,這里主要是為了防止主線程卡頓。
  • 4)捕捉音頻采集開始和停止操作中的錯(cuò)誤株依,拋給 KFAudioCapture 的對外錯(cuò)誤回調(diào)接口驱证。
    • -startRunning-stopRunning 方法中捕捉錯(cuò)誤,在 -callBackError: 方法向外回調(diào)恋腕。
  • 5)清理音頻采集實(shí)例抹锄。
    • -dealloc 方法中實(shí)現(xiàn)。

更具體細(xì)節(jié)見上述代碼及其注釋荠藤。

2伙单、采集音頻存儲為 PCM 文件

我們在一個(gè) ViewController 中來實(shí)現(xiàn)音頻采集邏輯并將采集的音頻存儲為 PCM 數(shù)據(jù)。

KFAudioCaptureViewController.m

#import "KFAudioCaptureViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "KFAudioCapture.h"

@interface KFAudioCaptureViewController ()
@property (nonatomic, strong) KFAudioConfig *audioConfig;
@property (nonatomic, strong) KFAudioCapture *audioCapture;
@property (nonatomic, strong) NSFileHandle *fileHandle;
@end

@implementation KFAudioCaptureViewController
#pragma mark - Property
- (KFAudioConfig *)audioConfig {
    if (!_audioConfig) {
        _audioConfig = [KFAudioConfig defaultConfig];
    }
    
    return _audioConfig;
}

- (KFAudioCapture *)audioCapture {
    if (!_audioCapture) {
        __weak typeof(self) weakSelf = self;
        _audioCapture = [[KFAudioCapture alloc] initWithConfig:self.audioConfig];
        _audioCapture.errorCallBack = ^(NSError* error) {
            NSLog(@"KFAudioCapture error: %zi %@", error.code, error.localizedDescription);
        };
        // 音頻采集數(shù)據(jù)回調(diào)哈肖。在這里將 PCM 數(shù)據(jù)寫入文件吻育。
        _audioCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
            if (sampleBuffer) {
                // 1、獲取 CMBlockBuffer牡彻,這里面封裝著 PCM 數(shù)據(jù)扫沼。
                CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
                size_t lengthAtOffsetOutput, totalLengthOutput;
                char *dataPointer;
                
                // 2出爹、從 CMBlockBuffer 中獲取 PCM 數(shù)據(jù)存儲到文件中。
                CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffsetOutput, &totalLengthOutput, &dataPointer);
                [weakSelf.fileHandle writeData:[NSData dataWithBytes:dataPointer length:totalLengthOutput]];
            }
        };
    }
    
    return _audioCapture;
}

- (NSFileHandle *)fileHandle {
    if (!_fileHandle) {
        NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.pcm"];
        NSLog(@"PCM file path: %@", audioPath);
        [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil];
        [[NSFileManager defaultManager] createFileAtPath:audioPath contents:nil attributes:nil];
        _fileHandle = [NSFileHandle fileHandleForWritingAtPath:audioPath];
    }

    return _fileHandle;
}

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setupAudioSession];
    [self setupUI];
    
    // 完成音頻采集后缎除,可以將 App Document 文件夾下面的 test.pcm 文件拷貝到電腦上严就,使用 ffplay 播放:
    // ffplay -ar 44100 -channels 2 -f s16le -i test.pcm
}

- (void)dealloc {
    if (_fileHandle) {
        [_fileHandle closeFile];
    }
}

#pragma mark - Setup
- (void)setupUI {
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.extendedLayoutIncludesOpaqueBars = YES;
    self.title = @"Audio Capture";
    self.view.backgroundColor = [UIColor whiteColor];
    
    
    // Navigation item.
    UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)];
    UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)];
    self.navigationItem.rightBarButtonItems = @[startBarButton, stopBarButton];

}

- (void)setupAudioSession {
    NSError *error = nil;
    
    // 1、獲取音頻會話實(shí)例器罐。
    AVAudioSession *session = [AVAudioSession sharedInstance];

    // 2梢为、設(shè)置分類和選項(xiàng)。
    [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error];
    if (error) {
        NSLog(@"AVAudioSession setCategory error.");
        error = nil;
        return;
    }
    
    // 3轰坊、設(shè)置模式铸董。
    [session setMode:AVAudioSessionModeVideoRecording error:&error];
    if (error) {
        NSLog(@"AVAudioSession setMode error.");
        error = nil;
        return;
    }

    // 4、激活會話肴沫。
    [session setActive:YES error:&error];
    if (error) {
        NSLog(@"AVAudioSession setActive error.");
        error = nil;
        return;
    }
}

#pragma mark - Action
- (void)start {
    [self.audioCapture startRunning];
}

- (void)stop {
    [self.audioCapture stopRunning];
}

@end

上面是 KFAudioCaptureViewController 的實(shí)現(xiàn)粟害,這里需要注意的是在采集音頻前需要設(shè)置 AVAudioSession[6] 為正確的采集模式。

3颤芬、用工具播放 PCM 文件

完成音頻采集后悲幅,可以將 App Document 文件夾下面的 test.pcm 文件拷貝到電腦上,使用 ffplay 播放來驗(yàn)證一下音頻采集是效果是否符合預(yù)期:

$ ffplay -ar 44100 -channels 2 -f s16le -i test.pcm

注意這里的參數(shù)要對齊在工程代碼中設(shè)置的采樣率站蝠、聲道數(shù)汰具、采樣位深

關(guān)于播放 PCM 文件的工具菱魔,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具《可視化音視頻分析工具》第 1.1 節(jié) Adobe Audition留荔。

參考資料

[1]CMSampleBufferRef: https://developer.apple.com/documentation/coremedia/cmsamplebufferref/

[2]CMSampleBuffer: https://developer.apple.com/documentation/coremedia/cmsamplebuffer-u71

[3]CMBlockBuffer: https://developer.apple.com/documentation/coremedia/cmblockbuffer-u9i

[4]CVImageBuffer: https://developer.apple.com/documentation/corevideo/cvimagebuffer-q40

[5]CVPixelBuffer: https://developer.apple.com/documentation/corevideo/cvpixelbuffer-q2e

[6]AVAudioSession: https://developer.apple.com/documentation/avfaudio/avaudiosession/

[6]

AVAudioSession: https://developer.apple.com/documentation/avfaudio/avaudiosession/

推薦閱讀

《FFmpeg 工具:音視頻開發(fā)都用它,快@你兄弟來看》

《可視化音視頻分析工具:好用工具大集錦澜倦,快轉(zhuǎn)發(fā)給你兄弟看看》

《數(shù)據(jù)抓包工具:看看競品的協(xié)議都做了哪些優(yōu)化》

《iOS 逆向工具:逆向做的好聚蝶,碼農(nóng)下班早》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市藻治,隨后出現(xiàn)的幾起案子既荚,更是在濱河造成了極大的恐慌,老刑警劉巖栋艳,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恰聘,死亡現(xiàn)場離奇詭異,居然都是意外死亡吸占,警方通過查閱死者的電腦和手機(jī)晴叨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來矾屯,“玉大人兼蕊,你說我怎么就攤上這事〖希” “怎么了孙技?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵产禾,是天一觀的道長。 經(jīng)常有香客問我牵啦,道長亚情,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任哈雏,我火速辦了婚禮楞件,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘裳瘪。我一直安慰自己土浸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布彭羹。 她就那樣靜靜地躺著黄伊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪派殷。 梳的紋絲不亂的頭發(fā)上毅舆,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機(jī)與錄音愈腾,去河邊找鬼。 笑死岂津,一個(gè)胖子當(dāng)著我的面吹牛虱黄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吮成,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼橱乱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了粱甫?” 一聲冷哼從身側(cè)響起泳叠,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茶宵,沒想到半個(gè)月后危纫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乌庶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年种蝶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞒大。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡螃征,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出透敌,到底是詐尸還是另有隱情盯滚,我是刑警寧澤踢械,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站魄藕,受9級特大地震影響内列,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泼疑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一德绿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧退渗,春花似錦移稳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至翻翩,卻和暖如春都许,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嫂冻。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工胶征, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桨仿。 一個(gè)月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓睛低,卻偏偏與公主長得像,于是被迫代替她去往敵國和親服傍。 傳聞我的和親對象是個(gè)殘疾皇子钱雷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

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