iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 本地平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
过程,并借助音视频工具来分析和理解对应的音视频数据。
在音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。
这里是第二篇:iOS 音频编码 Demo。这个 Demo 里包含以下内容:
-
1)实现一个音频采集模块;
-
2)实现一个音频编码模块;
-
3)串联音频采集和编码模块,将采集到的音频数据输入给 AAC 编码模块进行编码和存储;
-
4)详尽的代码注释,帮你理解代码逻辑和原理。
想要了解 AAC 编码,可以看看《音频编码:PCM 和 AAC 编码》。
1、音频采集模块
在这个 Demo 中,音频采集模块 KFAudioCapture
的实现与 《iOS 音频采集 Demo》 中一样,这里就不再重复介绍了,其接口如下:
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
2、音频编码模块
接下来,我们来实现一个音频编码模块 KFAudioEncoder
,在这里输入采集后的数据,输出编码后的数据。
KFAudioEncoder.h
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
NS_ASSUME_NONNULL_BEGIN
@interface KFAudioEncoder : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithAudioBitrate:(NSInteger)audioBitrate;
@property (nonatomic, assign, readonly) NSInteger audioBitrate; // 音频编码码率。
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频编码数据回调。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频编码错误回调。
- (void)encodeSampleBuffer:(CMSampleBufferRef)buffer; // 编码。
@end
NS_ASSUME_NONNULL_END
上面是 KFAudioEncoder
接口的设计,除了初始化方法
,主要是有获取音频编码码率
以及音频编码数据回调
和错误回调
的接口,另外就是编码
的接口。
其中编码
接口对应着音频编码模块输入,数据回调
接口则对应着输出。可以看到这里输入输出的参数都是 CMSampleBufferRef[1] 这个数据结构。它是对 CMSampleBuffer
的一个引用。
CMSampleBuffer
是 iOS 系统用来在音视频处理的 pipeline 中使用和传递媒体采样数据的核心数据结构。你可以认为它是 iOS 音视频处理 pipeline 中的流通货币,摄像头采集的视频数据接口、麦克风采集的音频数据接口、编码和解码数据接口、读取和存储视频接口、视频渲染接口等等,都以它作为参数。我们在 《iOS 音频采集 Demo》 一文中介绍音频采集接口的时候详细介绍过 CMSampleBuffer
,可以去看看回顾一下。
所以,在这里我们也以 CMSampleBufferRef
作为编码模块输入和输出的接口参数。
KFAudioEncoder.m
#import "KFAudioEncoder.h"
#import <AudioToolbox/AudioToolbox.h>
@interface KFAudioEncoder () {
char *_leftBuffer; // 待编码缓冲区。
NSInteger _leftLength; // 待编码缓冲区的长度,动态。
char *_aacBuffer; // 编码缓冲区。
NSInteger _bufferLength; // 每次送给编码器的数据长度。
}
@property (nonatomic, assign) AudioConverterRef audioEncoderInstance; // 音频编码器实例。
@property (nonatomic, assign) CMFormatDescriptionRef aacFormat; // 音频编码参数。
@property (nonatomic, assign, readwrite) NSInteger audioBitrate; // 音频编码码率。
@property (nonatomic, assign) BOOL isError;
@property (nonatomic, strong) dispatch_queue_t encoderQueue;
@end
@implementation KFAudioEncoder
#pragma mark - Lifecycle
- (instancetype)initWithAudioBitrate:(NSInteger)audioBitrate {
self = [super init];
if (self) {
_audioBitrate = audioBitrate;
_encoderQueue = dispatch_queue_create("com.KeyFrameKit.audioEncoder", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)dealloc {
// 清理编码器。
if (_audioEncoderInstance) {
AudioConverterDispose(_audioEncoderInstance);
_audioEncoderInstance = nil;
}
if (_aacFormat) {
CFRelease(_aacFormat);
_aacFormat = NULL;
}
// 清理缓冲区。
if (_aacBuffer) {
free(_aacBuffer);
_aacBuffer = NULL;
}
if (_leftBuffer) {
free(_leftBuffer);
_leftBuffer = NULL;
}
}
#pragma mark - Utility
- (void)setupAudioEncoderInstanceWithInputAudioFormat:(AudioStreamBasicDescription)input