我們将通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實作 Demo 來向大家介紹如何在 iOS/Android 平台上手音視訊開發。
這裡是第六篇:iOS 音頻渲染 Demo。這個 Demo 裡包含以下内容:
- 1)實作一個音頻解封裝子產品;
- 2)實作一個音頻解碼子產品;
- 3)實作一個音頻渲染子產品;
- 4)實作對 MP4 檔案中音頻部分的解封裝和解碼邏輯,并将解封裝、解碼後的資料送給渲染子產品播放;
- 5)詳盡的代碼注釋,幫你了解代碼邏輯和原理。
前五篇:
iOS要開發,采集音頻并存儲為 PCM 檔案
iOS音視訊開發二:音頻編碼,采集 PCM 資料編碼為 AAC
iOS音視訊開發三:音頻封裝,采集編碼并封裝為 M4A
iOS音視訊開發四:音頻解封裝,從 MP4 中解封裝出 AAC
iOS音視訊開發五:音頻解碼
1、音頻解封裝子產品
在這個 Demo 中,解封裝子產品
KFMP4Demuxer
的實作與 《iOS 音頻解封裝 Demo》 中一樣,這裡就不再重複介紹了。
2、音頻解碼子產品
同樣的,解封裝子產品
KFAudioDecoder
的實作與 《iOS 音頻解碼 Demo》 中一樣,這裡就不再重複介紹了。
3、音頻渲染子產品
接下來,我們來實作一個音頻渲染子產品
KFAudioRender
,在這裡輸入解碼後的資料進行渲染播放。
KFAudioRender.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@class KFAudioRender;
NS_ASSUME_NONNULL_BEGIN
@interface KFAudioRender : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate;
@property (nonatomic, copy) void (^audioBufferInputCallBack)(AudioBufferList *audioBufferList); // 音頻渲染資料輸入回調。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音頻渲染錯誤回調。
@property (nonatomic, assign, readonly) NSInteger audioChannels; // 聲道數。
@property (nonatomic, assign, readonly) NSInteger bitDepth; // 采樣位深。
@property (nonatomic, assign, readonly) NSInteger audioSampleRate; // 采樣率。
- (void)startPlaying; // 開始渲染。
- (void)stopPlaying; // 結束渲染。
@end
NS_ASSUME_NONNULL_END
上面是
KFAudioRender
接口的設計,除了
初始化
接口,主要是有音頻渲染
資料輸入回調
和
錯誤回調
的接口,另外就是
擷取聲道數
和
擷取采樣率
的接口,以及
開始渲染
和
結束渲染
的接口。
這裡重點需要看一下音頻渲染
資料輸入回調
接口,系統的音頻渲染單元每次會主動通過回調的方式要資料,我們這裡封裝的
KFAudioRender
則是用
資料輸入回調
接口來從外部擷取一組待渲染的音頻資料送給系統的音頻渲染單元。
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓
KFAudioRender.m
#import "KFAudioRender.h"
#define OutputBus 0
@interface KFAudioRender ()
@property (nonatomic, assign) AudioComponentInstance audioRenderInstance; // 音頻渲染執行個體。
@property (nonatomic, assign, readwrite) NSInteger audioChannels; // 聲道數。
@property (nonatomic, assign, readwrite) NSInteger bitDepth; // 采樣位深。
@property (nonatomic, assign, readwrite) NSInteger audioSampleRate; // 采樣率。
@property (nonatomic, strong) dispatch_queue_t renderQueue;
@property (nonatomic, assign) BOOL isError;
@end
@implementation KFAudioRender
#pragma mark - Lifecycle
- (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate {
self = [super init];
if (self) {
_audioChannels = channels;
_bitDepth = bitDepth;
_audioSampleRate = sampleRate;
_renderQueue = dispatch_queue_create("com.KeyFrameKit.audioRender", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)dealloc {
// 清理音頻渲染執行個體。
if (_audioRenderInstance) {
AudioOutputUnitStop(_audioRenderInstance);
AudioUnitUninitialize(_audioRenderInstance);
AudioComponentInstanceDispose(_audioRenderInstance);
_audioRenderInstance = nil;
}
}
#pragma mark - Action
- (void)startPlaying {
__weak typeof(self) weakSelf = self;
dispatch_async(_renderQueue, ^{
if (!weakSelf.audioRenderInstance) {
NSError *error = nil;
// 第一次 startPlaying 時建立音頻渲染執行個體。
[weakSelf _setupAudioRenderInstance:&error];
if (error) {
// 捕捉并回調建立音頻渲染執行個體時的錯誤。
[weakSelf _callBackError:error];
return;
}
}
// 開始渲染。
OSStatus status = AudioOutputUnitStart(weakSelf.audioRenderInstance);
if (status != noErr) {
// 捕捉并回調開始渲染時的錯誤。
[weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]];
}
});
}
- (void)stopPlaying {
__weak typeof(self) weakSelf = self;
dispatch_async(_renderQueue, ^{
if (weakSelf.audioRenderInstance && !self.isError) {
// 停止渲染。
OSStatus status = AudioOutputUnitStop(weakSelf.audioRenderInstance);
// 捕捉并回調停止渲染時的錯誤。
if (status != noErr) {
[weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]];
}
}
});
}
#pragma mark - Private Method
- (void)_setupAudioRenderInstance:(NSError**)error {
// 1、設定音頻元件描述。
AudioComponentDescription audioComponentDescription = {
.componentType = kAudioUnitType_Output,
//.componentSubType = kAudioUnitSubType_VoiceProcessingIO, // 回聲消除模式
.componentSubType = kAudioUnitSubType_RemoteIO,
.componentManufacturer = kAudioUnitManufacturer_Apple,
.componentFlags = 0,
.componentFlagsMask = 0
};
// 2、查找符合指定描述的音頻元件。
AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioComponentDescription);
// 3、建立音頻元件執行個體。
OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioRenderInstance);
if (status != noErr) {
*error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
return;
}
// 4、設定執行個體的屬性:可讀寫。0 不可讀寫,1 可讀寫。
UInt32 flag = 1;
status = AudioUnitSetProperty(_audioRenderInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, OutputBus, &flag, sizeof(flag));
if (status != noErr) {
*error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
return;
}
// 5、設定執行個體的屬性:音頻參數,如:資料格式、聲道數、采樣位深、采樣率等。
AudioStreamBasicDescription inputFormat = {0};
inputFormat.mFormatID = kAudioFormatLinearPCM; // 原始資料為 PCM,采用聲道交錯格式。
inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
inputFormat.mChannelsPerFrame = (UInt32) self.audioChannels; // 每幀的聲道數。
inputFormat.mFramesPerPacket = 1; // 每個資料包幀數。
inputFormat.mBitsPerChannel = (UInt32) self.bitDepth; // 采樣位深。
inputFormat.mBytesPerFrame = inputFormat.mChannelsPerFrame * inputFormat.mBitsPerChannel / 8; // 每幀位元組數 (byte = bit / 8)。
inputFormat.mBytesPerPacket = inputFormat.mFramesPerPacket * inputFormat.mBytesPerFrame; // 每個包位元組數。
inputFormat.mSampleRate = self.audioSampleRate; // 采樣率
status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, OutputBus, &inputFormat, sizeof(inputFormat));
if (status != noErr) {
*error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
return;
}
// 6、設定執行個體的屬性:資料回調函數。
AURenderCallbackStruct renderCallbackRef = {
.inputProc = audioRenderCallback,
.inputProcRefCon = (__bridge void *) (self) // 對應回調函數中的 *inRefCon。
};
status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, OutputBus, &renderCallbackRef, sizeof(renderCallbackRef));
if (status != noErr) {
*error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
return;
}
// 7、初始化執行個體。
status = AudioUnitInitialize(_audioRenderInstance);
if (status != noErr) {
*error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
return;
}
}
- (void)_callBackError:(NSError*)error {
self.isError = YES;
if (self.errorCallBack) {
dispatch_async(dispatch_get_main_queue(), ^{
self.errorCallBack(error);
});
}
}
#pragma mark - Render Callback
static OSStatus audioRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inOutputBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) {
// 通過音頻渲染資料輸入回調從外部擷取待渲染的資料。
KFAudioRender *audioRender = (__bridge KFAudioRender *) inRefCon;
if (audioRender.audioBufferInputCallBack) {
audioRender.audioBufferInputCallBack(ioData);
}
return noErr;
}
@end
上面是
KFAudioRender
的實作,從代碼上可以看到主要有這幾個部分:
- 1)建立音頻渲染執行個體。第一次調用
才會建立音頻渲染執行個體。-startPlaying
- 在
方法中實作。-_setupAudioRenderInstance:
- 在
- 2)處理音頻渲染執行個體的資料回調,并在回調中通過 KFAudioRender 的對外資料輸入回調接口向更外層要待渲染的資料。
- 在
方法中實作回調處理邏輯。通過audioRenderCallback(...)
回調接口向更外層要資料。audioBufferInputCallBack
- 在
- 3)實作開始渲染和停止渲染邏輯。
- 分别在
和-startPlaying
方法中實作。注意,這裡是開始和停止操作都是放在串行隊列中通過-stopPlaying
異步處理的,這裡主要是為了防止主線程卡頓。dispatch_async
- 分别在
- 4)捕捉音頻渲染開始和停止操作中的錯誤,抛給 KFAudioRender 的對外錯誤回調接口。
- 在
和-startPlaying
方法中捕捉錯誤,在-stopPlaying
方法向外回調。-_callBackError:
- 在
- 5)清理音頻渲染執行個體。
- 在
方法中實作。-dealloc
- 在
更具體細節見上述代碼及其注釋。
4、解封裝和解碼 MP4 檔案中的音頻部分并渲染播放
我們在一個 ViewController 中來實作從 MP4 檔案中解封裝和解碼音頻資料進行渲染播放。
KFAudioRenderViewController.m
#import "KFAudioRenderViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "KFAudioRender.h"
#import "KFMP4Demuxer.h"
#import "KFAudioDecoder.h"
#import "KFWeakProxy.h"
#define KFDecoderMaxCache 4096 * 5 // 解碼資料緩沖區最大長度。
@interface KFAudioRenderViewController ()
@property (nonatomic, strong) KFDemuxerConfig *demuxerConfig;
@property (nonatomic, strong) KFMP4Demuxer *demuxer;
@property (nonatomic, strong) KFAudioDecoder *decoder;
@property (nonatomic, strong) KFAudioRender *audioRender;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, strong) NSMutableData *pcmDataCache; // 解碼資料緩沖區。
@property (nonatomic, assign) NSInteger pcmDataCacheLength;
@property (nonatomic, strong) CADisplayLink *timer;
@end
@implementation KFAudioRenderViewController
#pragma mark - Property
- (KFDemuxerConfig *)demuxerConfig {
if (!_demuxerConfig) {
_demuxerConfig = [[KFDemuxerConfig alloc] init];
_demuxerConfig.demuxerType = KFMediaAudio;
NSString *videoPath = [[NSBundle mainBundle] pathForResource:@"input" ofType:@"mp4"];
_demuxerConfig.asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]];
}
return _demuxerConfig;
}
- (KFMP4Demuxer *)demuxer {
if (!_demuxer) {
_demuxer = [[KFMP4Demuxer alloc] initWithConfig:self.demuxerConfig];
_demuxer.errorCallBack = ^(NSError *error) {
NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription);
};
}
return _demuxer;
}
- (KFAudioDecoder *)decoder {
if (!_decoder) {
__weak typeof(self) weakSelf = self;
_decoder = [[KFAudioDecoder alloc] init];
_decoder.errorCallBack = ^(NSError *error) {
NSLog(@"KFAudioDecoder error:%zi %@", error.code, error.localizedDescription);
};
// 解碼資料回調。在這裡把解碼後的音頻 PCM 資料緩沖起來等待渲染。
_decoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
if (sampleBuffer) {
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t totolLength;
char *dataPointer = NULL;
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer);
if (totolLength == 0 || !dataPointer) {
return;
}
dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER);
[weakSelf.pcmDataCache appendData:[NSData dataWithBytes:dataPointer length:totolLength]];
weakSelf.pcmDataCacheLength += totolLength;
dispatch_semaphore_signal(weakSelf.semaphore);
}
};
}
return _decoder;
}
- (KFAudioRender *)audioRender {
if (!_audioRender) {
__weak typeof(self) weakSelf = self;
// 這裡設定的音頻聲道數、采樣位深、采樣率需要跟輸入源的音頻參數一緻。
_audioRender = [[KFAudioRender alloc] initWithChannels:1 bitDepth:16 sampleRate:44100];
_audioRender.errorCallBack = ^(NSError* error) {
NSLog(@"KFAudioRender error:%zi %@", error.code, error.localizedDescription);
};
// 渲染輸入資料回調。在這裡把緩沖區的資料交給系統音頻渲染單元渲染。
_audioRender.audioBufferInputCallBack = ^(AudioBufferList * _Nonnull audioBufferList) {
if (weakSelf.pcmDataCacheLength < audioBufferList->mBuffers[0].mDataByteSize) {
memset(audioBufferList->mBuffers[0].mData, 0, audioBufferList->mBuffers[0].mDataByteSize);
} else {
dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER);
memcpy(audioBufferList->mBuffers[0].mData, weakSelf.pcmDataCache.bytes, audioBufferList->mBuffers[0].mDataByteSize);
[weakSelf.pcmDataCache replaceBytesInRange:NSMakeRange(0, audioBufferList->mBuffers[0].mDataByteSize) withBytes:NULL length:0];
weakSelf.pcmDataCacheLength -= audioBufferList->mBuffers[0].mDataByteSize;
dispatch_semaphore_signal(weakSelf.semaphore);
}
};
}
return _audioRender;
}
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
_semaphore = dispatch_semaphore_create(1);
_pcmDataCache = [[NSMutableData alloc] init];
[self setupAudioSession];
[self setupUI];
// 通過一個 timer 來保證持續從檔案中解封裝和解碼一定量的資料。
_timer = [CADisplayLink displayLinkWithTarget:[KFWeakProxy proxyWithTarget:self] selector:@selector(timerCallBack:)];
[_timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_timer setPaused:NO];
[self.demuxer startReading:^(BOOL success, NSError * _Nonnull error) {
NSLog(@"KFMP4Demuxer start:%d", success);
}];
}
- (void)dealloc {
}
#pragma mark - Setup
- (void)setupUI {
self.edgesForExtendedLayout = UIRectEdgeAll;
self.extendedLayoutIncludesOpaqueBars = YES;
self.title = @"Audio Render";
self.view.backgroundColor = [UIColor whiteColor];
// Navigation item.
UIBarButtonItem *startRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(startRender)];
UIBarButtonItem *stopRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stopRender)];
self.navigationItem.rightBarButtonItems = @[startRenderBarButton, stopRenderBarButton];
}
#pragma mark - Action
- (void)startRender {
[self.audioRender startPlaying];
}
- (void)stopRender {
[self.audioRender stopPlaying];
}
#pragma mark - Utility
- (void)setupAudioSession {
// 1、擷取音頻會話執行個體。
AVAudioSession *session = [AVAudioSession sharedInstance];
// 2、設定分類。
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
if (error) {
NSLog(@"AVAudioSession setCategory error");
}
// 3、激活會話。
[session setActive:YES error:&error];
if (error) {
NSLog(@"AVAudioSession setActive error");
}
}
- (void)timerCallBack:(CADisplayLink *)link {
// 定時從檔案中解封裝和解碼一定量(不超過 KFDecoderMaxCache)的資料。
if (self.pcmDataCacheLength < KFDecoderMaxCache && self.demuxer.demuxerStatus == KFMP4DemuxerStatusRunning && self.demuxer.hasAudioSampleBuffer) {
CMSampleBufferRef audioBuffer = [self.demuxer copyNextAudioSampleBuffer];
if (audioBuffer) {
[self decodeSampleBuffer:audioBuffer];
CFRelease(audioBuffer);
}
}
}
- (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
// 擷取解封裝後的 AAC 編碼裸資料。
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t totolLength;
char *dataPointer = NULL;
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer);
if (totolLength == 0 || !dataPointer) {
return;
}
// 目前 AudioDecoder 的解碼接口實作的是單包(packet,1 packet 有 1024 幀)解碼。而從 Demuxer 擷取的一個 CMSampleBuffer 可能包含多個包,是以這裡要拆一下包,再送給解碼器。
NSLog(@"SampleNum: %ld", CMSampleBufferGetNumSamples(sampleBuffer));
for (NSInteger index = 0; index < CMSampleBufferGetNumSamples(sampleBuffer); index++) {
// 1、擷取一個包的資料。
size_t sampleSize = CMSampleBufferGetSampleSize(sampleBuffer, index);
CMSampleTimingInfo timingInfo;
CMSampleBufferGetSampleTimingInfo(sampleBuffer, index, &timingInfo);
char *sampleDataPointer = malloc(sampleSize);
memcpy(sampleDataPointer, dataPointer, sampleSize);
// 2、将資料封裝到 CMBlockBuffer 中。
CMBlockBufferRef packetBlockBuffer;
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
sampleDataPointer,
sampleSize,
NULL,
NULL,
0,
sampleSize,
0,
&packetBlockBuffer);
if (status == noErr) {
// 3、将 CMBlockBuffer 封裝到 CMSampleBuffer 中。
CMSampleBufferRef packetSampleBuffer = NULL;
const size_t sampleSizeArray[] = {sampleSize};
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
packetBlockBuffer,
CMSampleBufferGetFormatDescription(sampleBuffer),
1,
1,
&timingInfo,
1,
sampleSizeArray,
&packetSampleBuffer);
CFRelease(packetBlockBuffer);
// 4、解碼這個包的資料。
if (packetSampleBuffer) {
[self.decoder decodeSampleBuffer:packetSampleBuffer];
CFRelease(packetSampleBuffer);
}
}
dataPointer += sampleSize;
}
}
@end
上面是
KFAudioRenderViewController
的實作,其中主要包含這幾個部分:
- 1)在頁面加載完成後就啟動解封裝和解碼子產品,并通過一個 timer 來驅動解封裝器和解碼器。
- 在
中實作。-viewDidLoad
- 在
- 2)定時從檔案中解封裝一定量(不超過 KFDecoderMaxCache)的資料送給解碼器。
- 在
方法中實作。-timerCallBack:
- 在
- 3)解封裝後,需要将資料拆包,以包為機關封裝為
送給解碼器解碼。CMSampleBuffer
- 在
方法中實作。-decodeSampleBuffer:
- 在
- 4)在解碼子產品
的資料回調中擷取解碼後的 PCM 資料緩沖起來等待渲染。KFAudioDecoder
- 在
的KFAudioDecoder
回調中實作。sampleBufferOutputCallBack
- 在
- 5)在渲染子產品
的輸入資料回調中把緩沖區的資料交給系統音頻渲染單元渲染。KFAudioRender
- 在
的KFAudioRender
回調中實作。audioBufferInputCallBack
- 在
更具體細節見上述代碼及其注釋。
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓