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ā)給你兄弟看看》