天天看點

iOS要開發,采集音頻并存儲為 PCM 檔案1、音頻采集子產品

這裡是第一篇: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

,關于它有如下幾點需要注意:

  • CMSampleBuffer

     則是一個 Core Foundation 的對象,這意味着它的接口是 C 語言實作,它的記憶體管理是非 ARC 的,需要手動管理,它與 Foundation 對象之間需要進行橋接轉換。
  • CMSampleBuffer

     是系統用來在音視訊處理的 pipeline 中使用和傳遞媒體采樣資料的核心資料結構。你可以認為它是 iOS 音視訊處理 pipeline 中的流通貨币,攝像頭采集的視訊資料接口、麥克風采集的音頻資料接口、編碼和解碼資料接口、讀取和存儲視訊接口、視訊渲染接口等等,都以它作為參數。
  • CMSampleBuffer

     中包含着零個或多個某一類型(audio、video、muxed 等)的采樣資料。比如:
    • 要麼是一個或多個媒體采樣的 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)處理音頻采集執行個體的資料回調,并在回調中将資料封裝到 

    CMSampleBufferRef

     結構中,抛給 KFAudioCapture 的對外資料回調接口。
    • 在 

      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
           

 注意這裡的參數要對齊在工程代碼中設定的

采樣率

聲道數

采樣位深