在上一篇文章中介绍了iOS Audio Unit一些相关概念和基本用法,接下来通过阅读github上国外的一个开源的unit音频引擎框架源码,学习和理解Audio Unit技术在实际项目开发时可以有那些方式的应用场景,在代码结构和设计上有哪些考虑等等,加深自己对这块技术的理解,并且将其转化融入到自己的项目体系中去。我接触到的Audio方面的比较强大的开源库有AudioKit、TheAmazingAudioEngine、EZAudio、DouAudioStreamer,这些框架的侧重点各有不同,这里以TheAmazingAudioEngine为例,进行详细讲解。
TheAmazingAudioEngine
该框架是一个能同时运用在iOS、MAC OS上音频类APP的比较成熟的音频技术框架,其构建基于Core Audio Remote IO system,具备良好的性能及和低延迟处理能力,与AudioKit开源库提供丰富的工具集的方式不同,AmazingAudioEngine提供了包含音频采集、音效处理、播放等一整套解决方案,通过它可以快速的搭建出音频创作类的集成度高的APP。如果对音频处理理解不够深入的话,或者需要更基础的可灵活搭建特定场景的音频技术方案的话,使用AudioKit会更适合些。
下图是TheAmazingAudioEngine开源框架的概括类图,后面会分别详细介绍下几个核心模块的设计思路,以及如何将这些技术应用到自己的设计方案中去。
AmazingAudioEngineDiagram.jpg主要模块
AEBufferStack
该类是以栈的形式来存储和封装音频数据的缓冲池,在整个Engine流程里的各个环节之间交互的单元,例如向栈中压入缓冲采集音频数据;对当前存在的缓冲进行效果处理、解析、录制等;混合多路音频缓冲;输出缓冲到渲染;在所有处理结束后弹出释放缓冲。AEBufferStack相关结构定义如下:
const UInt32 AEBufferStackMaxFramesPerSlice = 4096;
static const int kDefaultPoolSize = 16;
typedef struct _AEBufferStackBufferLinkedList {
void * buffer;
struct _AEBufferStackBufferLinkedList * next;
} AEBufferStackPoolEntry;
typedef struct {
void * bytes;
AEBufferStackPoolEntry * free;
AEBufferStackPoolEntry * used;
} AEBufferStackPool;
struct AEBufferStack {
int poolSize;
int maxChannelsPerBuffer;
UInt32 frameCount;
int stackCount;
AEBufferStackPool audioPool;
AEBufferStackPool bufferListPool;
};
在AEBufferStack.h及.m文件中定义了许多操作Stack结构的helper方法,因为该BufferStatck是通过单链表结构实现,在压栈出栈等操作都会涉及到相应链接的节点操作,在外部使用基本只面对Stack中的AudioBufferList对象而不用考虑更多细节,该对象即AudioUnit中Render回调的基本单位。
AERenderer
该渲染类是整个引擎的核心类,主要负责驱动整个音频处理流程,通常位于音频生产和处理之间,简单说就是一个串连各种处理操作的驱动者,子渲染AESubRenderer也时常会在类似变速处理的场景下以中间处理流程的形式被使用。该类主要向外暴露AERenderLoopBlock回调,使用者在该回调内,对传递过来的AudioRenderContext中的音频数据施加处理,例如,创建一个AERenderer实例,并将其传入到AudioUnitOutput中,AudioUnitOutput运行后产生render callback回调到renderer提供的LoopBlock当中,你可以在该block中对即将输出的buffer进行Effect效果处理,回调结束后buffer被送到硬件播放。
AEAudioUnitOutput
该类是AEIOAudioUnit(真正封装AudioUnit的核心类)对外的封装类,与AERenderer结合,主要负责在Engine处理流程中音频数据的播放和采集,当然Engine框架中也提供了单独的AEAudioUnitInputModule来处理采集,因为在MAC OS与iOS上AudioUnit的设计方式不同(iOS平台上IO Unit的输入输出由单独的Unit提供,而在MAC上是两个独立的Unit提供)。
AEAudioFileOutput
该类封装了音频数据写入文件操作,与AERenderer结合,在写入文件之前可对音频数据进行效果处理,样例代码如下:
- (NSError *)createTestFile {
AERenderer * renderer = [AERenderer new];
__block NSError * error = nil;
AEAudioFileOutput * output = [[AEAudioFileOutput alloc] initWithRenderer:renderer URL:self.fileURL type:AEAudioFileTypeAIFFInt16 sampleRate:44100.0 channelCount:1 error:&error];
if ( !output ) {
return error;
}
AEOscillatorModule * osc = [[AEOscillatorModule alloc] initWithRenderer:renderer];
osc.frequency = 440.0;
renderer.block = ^(const AERenderContext * context) {
AEModuleProcess(osc, context);
AERenderContextOutput(context, 1);
};
__block BOOL done = NO;
[output runForDuration:kTestFileLength completionBlock:^(NSError * e){
done = YES;
error = e;
}];
while ( !done ) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
[output finishWriting];
return error;
}
AEModule
音频处理的基础单元,作为基类提供了统一的AEModuleProcessFunc处理方法地址,在render处理流程中通过调用AEModuleProcess(osc, context)传入具体module实例及音频buffer上下文进行同步音频处理,下面是其主要的派生类处理单元:
AEAudioUnitModule
继承自AEMoudle,音频基础单元与音效单元模块的基类,例如AEAudioFilePlayerModule、AEBandpassModule等都派生于此类,该基类封装了统一的AudioUnit创建、配置、处理入口(AEMoudle),在具体的处理模块中只需要指定其AudioUnitType及相关的配置参数即可。
AEMixerModule
混音处理单元,该类中持有普通处理模块(例如AEAudioFilePlayerModule)数组,在处理过程中会枚举其数组中的所有普通模块并且调用其processFunc函数,同时逐个混缩到StackBuffer中,并且可以逐个对音频单元进行音量及平衡处理。
AEAudioFilePlayerModule
音频播放类型的Unit模块,继承自AEModule,提供iOS平台所支持的全部格式的音频播放能力,主要集中在AEModuleProcessFunc,处理过程中,不断从音频文件中读取数据压入StackBuffer中,而且提供loop循环播放及开头结尾淡入淡出功能。
AEBandpassModule
音效处理模块之一,派生自AEAudioUnitModule,内部只定义了与Bandpass效果相关的参数设置和读取方法,其它音频处理模块与之类似。
AEManagedValue
封装对objective-c以及void*内存指针的操作和生命周期管理,通过pthread_mutex_t以及OSAtomic方式保证多线程访问安全性,以及在实时处理线程中避免加锁操作。
AERealtimeWatchdog
实时性看门狗,通过重载alloc、dealloc、socket send/recv等系统API调用,监听Audio Unit的工作线程AURemoteIO::IOThread中是否存在这些影响实时性的方法调用,并把这些调用打印出来,以alloc方法为例,下面是核心代码实现:
// Signatures for the functions we'll override
typedef void * (*malloc_t)(size_t);
// Overrides
void * malloc(size_t sz) {
static malloc_t funcptr = NULL;
if ( !funcptr ) funcptr = (malloc_t) dlsym(RTLD_NEXT, "malloc");
if ( AERealtimeWatchdogIsOnRealtimeThread() ) AERealtimeWatchdogUnsafeActivityWarning("malloc");
return funcptr(sz);
}
// print function
static void AERealtimeWatchdogUnsafeActivityWarning(const char * activity) {
#ifndef REPORT_EVERY_INFRACTION
static BOOL once = NO;
if ( !once ) {
once = YES;
#endif
printf("AERealtimeWatchdog: Caught unsafe %s on realtime thread. Put a breakpoint on %s to debug\n",
activity, __FUNCTION__);
#ifndef REPORT_EVERY_INFRACTION
}
#endif
}
// confirm the current thread is AURemote IO thread
BOOL AERealtimeWatchdogIsOnRealtimeThread(void);
BOOL AERealtimeWatchdogIsOnRealtimeThread(void) {
pthread_t thread = pthread_self();
char name[21] = {0};
if ( pthread_getname_np(thread, name, sizeof(name)) == 0 && !strcmp(name, "AURemoteIO::IOThread") ) {
return YES;
}
return NO;
}
AEAudioBufferListUtilities
该工具集封装了针对AudioBufferList常用基础操作和Stack操作,例如初始化、拷贝、释放等。
AEDSPUtilties
该工具集封装了一些Accelerate.framework\vDSP.h中提供的对音频信号处理的算法,例如增益处理、平滑处理、混音等操作,接口以AudioBufferList暴露方便调用
AEMainThreadEndpoint
封装了AECircularBuffer与NSThread类,提供主线程与音频处理线程进行消息或者数据传递的机制,CircularBuffer作为message的载体,在MainThreadEndPoint中实现了NSThread的main方法,通过semophore同步处理事件,由于CircularBuffer内部采用原子操作,保证多线程操作安全性
TheAmazingAudioEngine样例流程
下面的样例代码主要完成了3个音频文件的实时播放及录音功能,中间先对file1音频进行了带通效果处理,然后将file2和file3音频先混音,然后进行了延时效果处理,最后将所有buffer数据混音输出到硬件播放,并且录制到指定URL文件中
// Create our renderer and output
AERenderer * renderer = [AERenderer new];
self.output = [[AEAudioUnitOutput alloc] initWithRenderer:renderer];
// Create the players
AEAudioFilePlayerModule * file1 = [[AEAudioFilePlayerModule alloc] initWithRenderer:renderer URL:url1 error:NULL];
AEAudioFilePlayerModule * file2 = [[AEAudioFilePlayerModule alloc] initWithRenderer:renderer URL:url2 error:NULL];
AEAudioFilePlayerModule * file3 = [[AEAudioFilePlayerModule alloc] initWithRenderer:renderer URL:url3 error:NULL];
// Create the filters
AEBandpassModule * filter1 = [[AEBandpassModule alloc] initWithRenderer:renderer];
AEDelayModule * filter2 = [[AEDelayModule alloc] initWithRenderer:renderer];
// Create the recorder
AEAudioFileRecorderModule * recorder = [[AEAudioFileRecorderModule alloc] initWithRenderer:renderer URL:outputUrl error:NULL];
renderer.block = ^(const AERenderContext * _Nonnull context) {
AEModuleProcess(file1, context); // Run player (pushes 1)
AEModuleProcess(filter1, context); // Run filter (edits top buffer)
AEModuleProcess(file2, context); // Run player (pushes 1)
AEModuleProcess(file3, context); // Run player (pushes 1)
AEBufferStackMix(context->stack, 2); // Mix top 2 buffers
AEModuleProcess(filter2, context); // Run filter (edits top buffer)
AERenderContextOutput(context, 1); // Put top buffer onto output
AEModuleProcess(recorder, context); // Run recorder (uses top buffer)
};
[self.output start:NULL];
一些体会
通过对上面这个TheAmazingAudioEngine框架的学习和技术点的概要梳理,发现能学习到很多东西,很多技术点也是第一次碰到,非常有趣,这篇文章并没有仔细深挖,毕竟每个点都可以展开写一篇文章了,如果你感兴趣,自己下载阅读下代码吧。
另外,在音频这块的技术领域,还是要细分到很多方面的,例如专业音频播放APP、专业创作类、VOIP电话类、大型游戏类APP等等,不同类型的产品对所需音频技术的侧重点不尽相同,例如TheAmazingAudioEngine就是偏音频创作类APP的架构设计。没有万能的随处可用的引擎框架和设计,作为开发人员,还是应该将平台音频技术不同的点都学习透彻,零活运用到目标场景中去。
网友评论
源码在https://github.com/TheAmazingAudioEngine可找到