這裡是第一篇:iOS 音頻采集 Demo。這個 Demo 裡包含以下内容:
- 1)實作一個音頻采集子產品;
- 2)實作音頻采集邏輯并将采集的音頻存儲為 PCM 資料;
- 3)詳盡的代碼注釋,幫你了解代碼邏輯和原理。
1、音頻采集子產品
首先,實作一個 KFAudioConfig 類用于定義音頻采集參數的配置。這裡包括了:采樣率、量化位深、聲道數這幾個參數。
KFAudioConfig.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KFAudioConfig : NSObject
+ (instancetype)defaultConfig;
@property (nonatomic, assign) NSUInteger channels; // 聲道數,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
接下來,我們實作一個
KFAudioCapture
類來實作音頻采集。
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); // 音頻采集資料回調。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音頻采集錯誤回調。
- (void)startRunning; // 開始采集音頻資料。
- (void)stopRunning; // 停止采集音頻資料。
@end
NS_ASSUME_NONNULL_END
上面是
KFAudioCapture
的接口設計,可以看到這裡除了
初始化方法
,主要是有
擷取音頻配置
以及音頻采集
資料回調
和
錯誤回調
的接口,另外就是
開始采集
和
停止采集
的接口。
在上面的音頻采集
資料回調
接口中,我們傳回的是 CMSampleBufferRef[1] 這個資料結構,這裡我們重點介紹一下。官方文檔對
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] 的一個引用。所裡這裡核心的資料結構是
CMSampleBuffer
,關于它有如下幾點需要注意:
-
則是一個 Core Foundation 的對象,這意味着它的接口是 C 語言實作,它的記憶體管理是非 ARC 的,需要手動管理,它與 Foundation 對象之間需要進行橋接轉換。CMSampleBuffer
-
是系統用來在音視訊處理的 pipeline 中使用和傳遞媒體采樣資料的核心資料結構。你可以認為它是 iOS 音視訊處理 pipeline 中的流通貨币,攝像頭采集的視訊資料接口、麥克風采集的音頻資料接口、編碼和解碼資料接口、讀取和存儲視訊接口、視訊渲染接口等等,都以它作為參數。CMSampleBuffer
-
中包含着零個或多個某一類型(audio、video、muxed 等)的采樣資料。比如:CMSampleBuffer
- 要麼是一個或多個媒體采樣的 CMBlockBuffer[3]。其中可以封裝:音頻采集後、編碼後、解碼後的資料(如:PCM 資料、AAC 資料);視訊編碼後的資料(如:H.264 資料)。
- 要麼是一個 CVImageBuffer[4](也作 CVPixelBuffer[5])。其中包含媒體流中 CMSampleBuffers 的格式描述、每個采樣的寬高和時序資訊、緩沖級别和采樣級别的附屬資訊。緩沖級别的附屬資訊是指緩沖區整體的資訊,比如播放速度、對後續緩沖資料的操作等。采樣級别的附屬資訊是指單個采樣的資訊,比如視訊幀的時間戳、是否關鍵幀等。其中可以封裝:視訊采集後、解碼後等未經編碼的資料(如:YCbCr 資料、RGBA 資料)。
是以,了解完這些,就知道上面的音頻采集
資料回調
接口為什麼會傳回
CMSampleBufferRef
這個資料結構了。因為它通用,同時我們也可以從裡面擷取到我們想要的 PCM 資料。
KFAudioCapture.m
#import "KFAudioCapture.h"
#import <AVFoundation/AVFoundation.h>
#import <mach/mach_time.h>
@interface KFAudioCapture ()
@property (nonatomic, assign) AudioComponentInstance audioCaptureInstance; // 音頻采集執行個體。
@property (nonatomic, assign) AudioStreamBasicDescription audioFormat; // 音頻采集參數。
@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 {
// 清理音頻采集執行個體。
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 時建立音頻采集執行個體。
[weakSelf setupAudioCaptureInstance:&error];
if (error) {
// 捕捉并回調建立音頻執行個體時的錯誤。
[weakSelf callBackError:error];
return;
}
}
// 開始采集。
OSStatus startStatus = AudioOutputUnitStart(weakSelf.audioCaptureInstance);
if (startStatus != noErr) {
// 捕捉并回調開始采集時的錯誤。
[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) {
// 捕捉并回調停止采集時的錯誤。
[weakSelf callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioCapture class]) code:stopStatus userInfo:nil]];
}
}
});
}
#pragma mark - Utility
- (void)setupAudioCaptureInstance:(NSError **)error {
// 1、設定音頻元件描述。
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、建立音頻元件執行個體。
OSStatus status = AudioComponentInstanceNew(component, &_audioCaptureInstance);
if (status != noErr) {
*error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
return;
}
// 4、設定執行個體的屬性:可讀寫。0 不可讀寫,1 可讀寫。
UInt32 flagOne = 1;
AudioUnitSetProperty(_audioCaptureInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flagOne, sizeof(flagOne));
// 5、設定執行個體的屬性:音頻參數,如:資料格式、聲道數、采樣位深、采樣率等。
AudioStreamBasicDescription asbd = {0};
asbd.mFormatID = kAudioFormatLinearPCM; // 原始資料為 PCM,采用聲道交錯格式。
asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
asbd.mChannelsPerFrame = (UInt32) self.config.channels; // 每幀的聲道數
asbd.mFramesPerPacket = 1; // 每個資料包幀數
asbd.mBitsPerChannel = (UInt32) self.config.bitDepth; // 采樣位深
asbd.mBytesPerFrame = asbd.mChannelsPerFrame * asbd.mBitsPerChannel / 8; // 每幀位元組數 (byte = bit / 8)
asbd.mBytesPerPacket = asbd.mFramesPerPacket * asbd.mBytesPerFrame; // 每個包的位元組數
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、設定執行個體的屬性:資料回調函數。
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、初始化執行個體。
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 執行個體的引用。
// 1、建立音頻流的格式描述資訊。
CMFormatDescriptionRef format = NULL;
OSStatus status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &description, 0, NULL, 0, NULL, NULL, &format);
if (status != noErr) {
CFRelease(format);
return nil;
}
// 2、處理音頻幀的時間戳資訊。
mach_timebase_info_data_t info = {0, 0};
mach_timebase_info(&info);
uint64_t time = inTimeStamp->mHostTime;
// 轉換為納秒。
time *= info.numer;
time /= info.denom;
// PTS。
CMTime presentationTime = CMTimeMake(time, 1000000000.0f);
// 對于音頻,PTS 和 DTS 是一樣的。
CMSampleTimingInfo timing = {CMTimeMake(1, description.mSampleRate), presentationTime, presentationTime};
// 3、建立 CMSampleBuffer 執行個體。
status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, format, (CMItemCount) inNumberFrames, 1, &timing, 0, NULL, &sampleBuffer);
if (status != noErr) {
CFRelease(format);
return nil;
}
// 4、建立 CMBlockBuffer 執行個體。其中資料拷貝自 AudioBufferList,并将 CMBlockBuffer 執行個體關聯到 CMSampleBuffer 執行個體。
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、建立 AudioBufferList 空間,用來接收采集回來的資料。
AudioBuffer buffer;
buffer.mData = NULL;
buffer.mDataByteSize = 0;
// 采集的時候設定了資料格式是 kAudioFormatLinearPCM,即聲道交錯格式,是以即使是雙聲道這裡也設定 mNumberChannels 為 1。
// 對于雙聲道的資料,會按照采樣位深 16 bit 每組,一組接一組地進行兩個聲道資料的交錯拼裝。
buffer.mNumberChannels = 1;
AudioBufferList buffers;
buffers.mNumberBuffers = 1;
buffers.mBuffers[0] = buffer;
// 2、擷取音頻 PCM 資料,存儲到 AudioBufferList 中。
// 這裡有幾個問題要說明清楚:
// 1)每次回調會過來多少資料?
// 按照上面采集音頻參數的設定:PCM 為聲道交錯格式、每幀的聲道數為 2、采樣位深為 16 bit。這樣每幀的位元組數是 4 位元組(左右聲道各 2 位元組)。
// 傳回資料的幀數是 inNumberFrames。這樣一次回調回來的資料位元組數是多少就是:mBytesPerFrame(4) * inNumberFrames。
// 2)這個資料回調的頻率跟音頻采樣率有關系嗎?
// 這個資料回調的頻率與音頻采樣率(上面設定的 mSampleRate 44100)是沒關系的。聲道數、采樣位深、采樣率共同決定了裝置機關時間裡采樣資料的大小,這些資料是會緩沖起來,然後一塊一塊的通過這個資料回調給我們,這個回調的頻率是底層一塊一塊給我們資料的速度,跟采樣率無關。
// 3)這個資料回調的頻率是多少?
// 這個資料回調的間隔是 [AVAudioSession sharedInstance].preferredIOBufferDuration,頻率即該值的倒數。我們可以通過 [[AVAudioSession sharedInstance] setPreferredIOBufferDuration:1 error:nil] 設定這個值來控制回調頻率。
OSStatus status = AudioUnitRender(capture.audioCaptureInstance,
ioActionFlags,
inTimeStamp,
inBusNumber,
inNumberFrames,
&buffers);
// 3、資料封裝及回調。
if (status == noErr) {
// 使用工具方法将資料封裝為 CMSampleBuffer。
CMSampleBufferRef sampleBuffer = [KFAudioCapture sampleBufferFromAudioBufferList:buffers inTimeStamp:inTimeStamp inNumberFrames:inNumberFrames description:capture.audioFormat];
// 回調資料。
if (capture.sampleBufferOutputCallBack) {
capture.sampleBufferOutputCallBack(sampleBuffer);
}
if (sampleBuffer) {
CFRelease(sampleBuffer);
}
}
return status;
}
}
@end
上面是
KFAudioCapture
的實作,從代碼上可以看到主要有這幾個部分:
- 1)建立音頻采集執行個體。第一次調用
才會建立音頻采集執行個體。-startRunning
- 在
方法中實作。-setupAudioCaptureInstance:
- 在
- 2)處理音頻采集執行個體的資料回調,并在回調中将資料封裝到
結構中,抛給 KFAudioCapture 的對外資料回調接口。CMSampleBufferRef
- 在
方法中實作回調處理邏輯。audioBufferCallBack(...)
- 其中封裝
用到了CMSampleBufferRef
方法。+sampleBufferFromAudioBufferList:inTimeStamp:inNumberFrames:description:
- 在
- 3)實作開始采集和停止采集邏輯。
- 分别在
和-startRunning
方法中實作。注意,這裡是開始和停止操作都是放在串行隊列中通過-stopRunning
異步處理的,這裡主要是為了防止主線程卡頓。dispatch_async
- 分别在
- 4)捕捉音頻采集開始和停止操作中的錯誤,抛給 KFAudioCapture 的對外錯誤回調接口。
- 在
和-startRunning
方法中捕捉錯誤,在-stopRunning
方法向外回調。-callBackError:
- 在
- 5)清理音頻采集執行個體。
- 在
方法中實作。-dealloc
- 在
更具體細節見上述代碼及其注釋。
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓
2、采集音頻存儲為 PCM 檔案
我們在一個 ViewController 中來實作音頻采集邏輯并将采集的音頻存儲為 PCM 資料。
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);
};
// 音頻采集資料回調。在這裡将 PCM 資料寫入檔案。
_audioCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
if (sampleBuffer) {
// 1、擷取 CMBlockBuffer,這裡面封裝着 PCM 資料。
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t lengthAtOffsetOutput, totalLengthOutput;
char *dataPointer;
// 2、從 CMBlockBuffer 中擷取 PCM 資料存儲到檔案中。
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、擷取音頻會話執行個體。
AVAudioSession *session = [AVAudioSession sharedInstance];
// 2、設定分類和選項。
[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error];
if (error) {
NSLog(@"AVAudioSession setCategory error.");
error = nil;
return;
}
// 3、設定模式。
[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
的實作,這裡需要注意的是在采集音頻前需要設定 AVAudioSession[6] 為正确的采集模式。
3、用工具播放 PCM 檔案
完成音頻采集後,可以将 App Document 檔案夾下面的
test.pcm
檔案拷貝到電腦上,使用
ffplay
播放來驗證一下音頻采集是效果是否符合預期:
$ ffplay -ar 44100 -channels 2 -f s16le -i test.pcm
注意這裡的參數要對齊在工程代碼中設定的
采樣率
、
聲道數
、
采樣位深
。