attachthreadinput

attachthreadinput

最近一直在做iOS音频相关技术的项目,从官方和网上的文档中学到了很多。当然,iOS平台中还有很多方面的音频相关技术。这里我先做一个整体的概述,然后以I/O音频单元为例来说明它的概念、基本用法和思路。可能不够全面,有些细节需要自己去发现。稍后,我将分析github上的一个开源音频引擎框架的源代码,以展示在更复杂的音频技术应用场景中可能的设计和实现。

本文中的图片和大部分技术概念均来自苹果官网。

1、Core Audio

核心音频是iOS和MAC系统中数字音频处理的基础设施。它是应用程序用来处理音频的一组软件框架。iOS音频开发的所有接口都由Core Audio提供或封装。苹果官方给出的Core Audio层级图如下:

2.低级这主要用于在MAC上实现音频应用程序,并要求最大的实时性能。大多数音频app不需要使用这一层的服务。而且iOS上还提供了高实时性的高级API来满足你的需求。比如OpenAL,具有直接调用I/O I/O Kit,与硬件驱动交互的实时音频处理能力,游戏中的音频硬件抽象层,将API调用与实际硬件分离,维护独立的核心MIDI,为MIDI流和设备提供软件抽象工作层主机时间服务,访问计算机硬件时钟。

3、Mid-Level

该层具有完整的功能,包括音频数据格式转换、音频文件读写、音频流解析、音频转换服务等插件工作支持,负责音频数据格式的转换。音频文件服务负责读取和写入音频数据。音频单元服务和音频处理图形服务是支持均衡器和混音器等数字信号处理的插件Audi。o文件尖叫服务负责流解析,核心音频时钟服务负责音频时钟同步。

4、High-Level

是一组由低级接口组合而成的高级应用,基本上我们在音频开发上的很多工作都可以在这个层次上完成。音频队列服务提供录音、播放、暂停、循环和同步音频。它自动采用必要的编解码器来处理压缩的音频格式。AudioPlayer是专门为IOS平台提供的基于Objective-C接口的音频播放类。可以支持iOS支持的所有音频的播放。扩展音频文件服务由音频文件和音频转换器组成,提供压缩和未压缩音频文件的读写能力。OpenAL是CoreAudio对OpenAL标准的实现,可以播放3D混音效果。

5、不同场景所需要的API Service

只能播放音频,没有其他需求,AudioPlayer可以满足需求。它的界面使用起来很简单,你不需要关心细节。通常你只给它提供一个播放源的URL地址,调用它的播放、暂停、停止等方法来控制它。observer可以在回放状态下更新它的UI。

APP需要流音频,所以需要AudioFileStreamer和AudioQueue将网络或本地流读入内存,提交给AudioFileStreamer进行音频帧的分析和分离。分离后的音频帧可以发送到音频队列进行解码和播放。请参考音频流免费流AFSoundManager。

APP需要将音效(均衡器、混响器)应用于音频,即除了读取和分析数据外,还需要AudioConverter或Codec将音频数据转换为PCM数据,然后使用AudioUnit+AUGraph处理和播放音效。请参考DOU Audio Streamer音频引擎音频套件。

6、Audio Unit

ioS提供了混音、均衡、格式转换、实时IO录制、回放、离线渲染、VoIP等音频处理插件。它们都属于不同的AudioUnit,支持动态加载和使用。AudioUnit可以单独创建和使用,但它更多的是用在音频处理图形容器中,以满足各种处理需求,例如下面的场景:

APP持有的音频处理图形容器包含两个EQ单元,一个调音台单元和一个I/O单元。APP通过EQ单元均衡磁盘或网络上的两个数据流,然后在Mixer单元混合成一个流,再进入I/O单元将数据发送到硬件进行回放。在这个全过程中,APP可以随时调整和设置AU图和各个单元的工作状态和参数,动态访问或移除指定单元,保证线程安全。

如何免费获取C++音视频学习资料:关注音视频开发,点击“链接”即可免费获取2023年C++音视频开发最新独家免费学习包!

6.1 Audio Unit类型:

I/O:远程I/O、语音处理I/O、通用输出混合:3D混合器、多通道混合器效果:iPod均衡器格式转换:格式转换器

6.2 AudioUnit构建方式

创建音频单元有两种方法。以I/O单元为例。一种方法是直接调用单元接口,另一种方法是创建音频单元图。以下是两种方式的基本流程和相关代码:

6.3 Unit API方式(Remote IO Unit)

//创建IO单元BOOL结果=否;AudioComponentDescription output description = { 0 };Output description . component type = kAudioUnitType _ Output;output description . component subtype = kAudioUnitSubType _ remote io;output description . component manufacturer = kAudioUnitManufacturer _ Apple;output description . component flags = 0;output description . component flags mask = 0;audio component comp = AudioComponentFindNext(NULL,& output description);result = CheckOSStatus(audiocomponentinscenew(comp,&mVoipUnit),@ & # 34;不能& # 39;t创建RemoteIO的新实例& # 34;);如果(!result)返回结果;// config IO使能状态uint 32 flag = 1;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioOutputUnitProperty _ enable io,kAudioUnitScope_Output,kOutputBus,&flag,sizeof(flag)),@ & # 34;无法在RemoteIO上启用输出& # 34;);如果(!result)返回结果;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioOutputUnitProperty _ enable io,kAudioUnitScope_Input,kInputBus,&flag,sizeof(flag)),@ & # 34;AudioUnitSetProperty EnableIO & # 34;);如果(!result)返回结果;// Config默认格式result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ stream format,kAudioUnitScope_Output,kInputBus,& inputAudioDescription,sizeof(inputudiodescription)),@ & # 34;不能& # 39;t在RemoteIO上设置输入客户端格式& # 34;);如果(!result)返回结果;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ stream format,kAudioUnitScope_Input,kOutputBus,&outputAudioDescription,sizeof(outputAudioDescription)),@ & # 34;不能& # 39;t在RemoteIO上设置输出客户端格式& # 34;);如果(!result)返回结果;//设置MaximumFramesPerSlice属性。此属性用于向音频单元描述在任何一次给定的对audio unit render uint 32 maxFramesPerSlice = 4096的调用中它将被要求产生的最大//样本数;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ MaximumFramesPerSlice,kAudioUnitScope_Global,0,&maxFramesPerSlice,sizeof(UInt32)),@ & # 34;不能& # 39;t在RemoteIO上设置每个切片的最大帧数& # 34;);如果(!result)返回结果;//设置记录回调aurendcallbackruct record callback;record callback . input proc = recordCallbackFunc;record callback . inputprocrefcon =(_ _ bridge void * _ Nullable)(self);result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioOutputUnitProperty _ SetInputCallback,kAudioUnitScope_Global,kInputBus,&recordCallback,sizeof(recordCallback)),@ & # 34;不能& # 39;t在RemoteIO上设置记录回调& # 34;);如果(!result)返回结果;//设置播放回调aurendcallbackruct playback callback;playback callback . input proc = playbackcallback func;playback callback . inputprocrefcon =(_ _ bridge void * _ Nullable)(self);result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ SetRenderCallback,kAudioUnitScope_Global,kOutputBus,&playbackCallback,sizeof(playbackCallback)),@ & # 34;不能& # 39;t在RemoteIO上设置播放回调& # 34;);如果(!result)返回结果;//设置缓冲区分配标志= 0;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ shouldlocatebuffer,kAudioUnitScope_Output,kInputBus,&flag,sizeof(flag)),@ & # 34;不能& # 39;t设置ShouldAllocateBuffer的属性& # 34;);如果(!result)返回结果;//初始化输出IO实例result = CheckOSStatus(audiouninitialize(mVoipUnit),@ & # 34;不能& # 39;t初始化VoiceProcessingIO实例& # 34;);如果(!result)返回结果;返回YES6.4 AU Graph模式(多通道混音器单元+远程IO单元)//Create Augraph bool result = No;result = CheckOSStatus(new augraph(& processing graph),@ & # 34;不能& # 39;t创建AUGraph的新实例& # 34;);如果(!result)返回结果;// I/O单元AudioComponentDescription iOUnitDescription;iounitdescription . component type = kAudioUnitType _ Output;iounitdescription . component subtype = kAudioUnitSubType _ remote io;iounitdescription . component manufacturer = kAudioUnitManufacturer _ Apple;iounitdescription . component flags = 0;iounit description . component flags mask = 0;//多声道混音器单元AudioComponentDescription MixerUnitDescription;mixerunitdescription . component type = kAudioUnitType _ Mixer;mixerunitdescription . component subtype = kAudioUnitSubType _ multichannel mixer;mixerunitdescription . component manufacturer = kAudioUnitManufacturer _ Apple;mixerunitdescription . component flags = 0;mixerunitdescription . component flags mask = 0;AUNode iONode//I/O单元的节点AUNode mixerNode//多通道混音器单元的节点result = CheckOSStatus(AUGraphAddNode(processing graph,&iOUnitDescription,&iONode),@ & # 34;不能& # 39;t添加kAudioUnitSubType _ remote io & # 34;);如果(!result)返回结果;result = CheckOSStatus(AUGraphAddNode(processing graph,& MixerUnitDescription,&mixerNode),@ & # 34;不能& # 39;t添加混音器单元的节点实例& # 34;);如果(!result)返回结果;//打开AUGraph result = CheckOSStatus(AUGraph open(processing graph),@ & # 34;不能& # 39;t获取混音器单元的实例& # 34;);如果(!result)返回结果;//获取单元实例结果= CheckOSStatus(AUGraphNodeInfo(processing graph,mixerNode,NULL,& mMixerUnit),@ & # 34;不能& # 39;t获取混音器单元的实例& # 34;);如果(!result)返回结果;result = CheckOSStatus(AUGraphNodeInfo(processing graph,iONode,NULL,&mVoipUnit),@ & # 34;不能& # 39;t获取remoteio单元的新实例& # 34;);如果(!result)返回结果;//////////////////////////////////////////////////////////////uint 32 bus count = 2;//混音器单元输入的总线计数uint 32 guitar bus = 0;//调音台单元总线0会是立体声,会取吉他音uint 32 beats bus = 1;//调音台单元总线1将为单声道,并将取beats sound result = CheckOSStatus(audiunitsetproperty(mmixerrunit,kAudioUnitProperty _ element count,kAudioUnitScope_Input,0,&busCount,sizeof (busCount)),@ & # 34;无法设置混音器单元输入总线计数& # 34;);如果(!result)返回结果;uint 32 maximumFramesPerSlice = 4096;result = CheckOSStatus(audiunitsetproperty(mmixerrunit,kAudioUnitProperty _ MaximumFramesPerSlice,kAudioUnitScope_Global,0,&maximumFramesPerSlice,sizeof (maximumFramesPerSlice)),@ & # 34;无法设置每个切片的混音器单元最大帧数& # 34;);如果(!result)返回结果;//将输入呈现回调和上下文附加到(uint 16 bus number = 0;总线编号& ltbusCount++busNumber) { //设置包含输入呈现回调aurendcallbackruct playback callback的结构;playback callback . input proc = playbackcallback func;playback callback . inputprocrefcon =(_ _ bridge void * _ Nullable)(self);NSLog(@ & # 34;将渲染回调注册到混音器单元输入总线% u & # 34,bus number);//设置指定节点的回调& # 39;s指定的输入结果= CheckOSStatus(AUGraphSetNodeInputCallback(processing graph,mixerNode,busNumber,&playbackCallback),@ & # 34;不能& # 39;t在混音器单元上设置回放回调& # 34;);如果(!result)返回结果;} //配置混音器单元输入默认格式result = CheckOSStatus(audiunitsetproperty(mmixerrunit,kAudioUnitProperty _ stream format,kAudioUnitScope_Input,guitarBus,&outputAudioDescription,sizeof (outputAudioDescription)),@ & # 34;不能& # 39;t在混音器单元上设置输入0客户端格式& # 34;);如果(!result)返回结果;result = CheckOSStatus(audiunitsetproperty(mmixerrunit,kAudioUnitProperty _ stream format,kAudioUnitScope_Input,beatus,&outputAudioDescription,sizeof (outputAudioDescription)),@ & # 34;不能& # 39;t在混音器单元上设置输入1客户端格式& # 34;);如果(!result)返回结果;float 64 graphSampleRate = 44100.0;//赫兹;result = CheckOSStatus(audiunitsetproperty(mmixerrunit,kAudioUnitProperty_SampleRate,kAudioUnitScope_Output,0,&graphSampleRate,sizeof (graphSampleRate)),@ & # 34;不能& # 39;t在混音器单元上设置输出客户端格式& # 34;);如果(!result)返回结果;////////////////////////////////////////////////////////////////////////////////////////config void单元IO启用状态uint 32 flag = 1;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioOutputUnitProperty _ enable io,kAudioUnitScope_Output,kOutputBus,&flag,sizeof(flag)),@ & # 34;无法在kAudioUnitSubType_RemoteIO上启用输出& # 34;);如果(!result)返回结果;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioOutputUnitProperty _ enable io,kAudioUnitScope_Input,kInputBus,&flag,sizeof(flag)),@ & # 34;无法在kAudioUnitSubType_RemoteIO上启用输入& # 34;);如果(!result)返回结果;// config voip单元默认格式result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ stream format,kAudioUnitScope_Output,kInputBus,& inputAudioDescription,sizeof(inputudiodescription)),@ & # 34;不能& # 39;t在kAudioUnitSubType _ remote io & # 34;);如果(!result)返回结果;UInt32 maxFramesPerSlice = 4096result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ MaximumFramesPerSlice,kAudioUnitScope_Global,0,&maxFramesPerSlice,sizeof(UInt32)),@ & # 34;不能& # 39;t在kAudioUnitSubType _ remote io & # 34;);如果(!result)返回结果;//设置记录回调aurendcallbackruct record callback;record callback . input proc = recordCallbackFunc;record callback . inputprocrefcon =(_ _ bridge void * _ Nullable)(self);result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioOutputUnitProperty _ SetInputCallback,kAudioUnitScope_Global,kInputBus,&recordCallback,sizeof(recordCallback)),@ & # 34;不能& # 39;t在kAudioUnitSubType_RemoteIO上设置记录回调& # 34;);如果(!result)返回结果;//设置缓冲区分配标志= 0;result = CheckOSStatus(audiunitsetproperty(mVoipUnit,kAudioUnitProperty _ shouldlocatebuffer,kAudioUnitScope_Output,kInputBus,&flag,sizeof(flag)),@ & # 34;不能& # 39;t设置ShouldAllocateBuffer的属性& # 34;);如果(!result)返回结果;////////////////////////////////////////////////////初始化输出IO实例result = CheckOSStatus(AUGraphConnectNodeInput(processing graph,mixerNode,// source node 0,// source不能& # 39;t将ionode连接到mixernode & # 34);如果(!result)返回结果;result = CheckOSStatus(AUGraphInitialize(processing graph),@ & # 34;AUGraphInitialize失败& # 34;);如果(!result)返回结果;返回YES6.5音频单元数据的输入输出方式在处理音频数据时,每个单元都要经过一个输入输出的过程,输入输出音频格式都是设定好的(可以相同也可以不同)。两个单元之间的对接意味着将一个单元的输入连接到另一个单元的输出,或者将一个单元的输出连接到另一个单元的输入。需要注意的是,在扩展坞要保证音频格式的一致性。以远程I/O单元为例,结构如下图所示:

一个I/O单元包含两个实体对象,这两个实体对象(元素0,元素1)相互独立。可以根据需要通过kaudioutputunitproperty _ enableo属性打开和关闭它们。元素1连接到硬件输入,元素1的输入范围对你是不可见的。只能读取其输出域的数据,设置其输出域的音频格式。元素0连接到硬件输出,元素0的输出范围对您是不可见的。您只能写入其输入字段的数据并设置其输入字段的音频格式。

如何捕捉输入设备采集的数据,如何将处理后的数据发送到输出设备?通过AURenderCallbackStruct结构,将两个已定义的静态回调方法的地址设置为所需的元素0/1。单元配置运行时,单元调度线程会根据当前设备状态和音频格式安排调度周期,循环调用您提供的录音和回放回调方法。示例代码如下:

//录音回调,从bufferliststatic OSStatus recordCallbackFunc(void * in refcon,audiunitrenderactionflags * ioActionFlags,const audio timestamp * inTimeStamp,UInt32 inBusNumber,UInt32 inNumberFrames,AudioBufferList * io data){ ASAudioEngineSingleU * engine =(_ _ bridge ASAudioEngineSingleU *)in refcon;OSStatus err = noErrif(engine . audiochainieingreconstructed = = NO){ @ autorelease pool { AudioBufferList buf list =[engine get buffer list:inNumberFrames];err = AudioUnitRender([引擎记录单元],ioActionFlags,inTimeStamp,inBusNumber,inNumberFrames,& buf list);if(err){ HMLogDebug(LogModuleAudio,@ & # 34;AudioUnitRender错误代码= % d & # 34,err);} else { audio buffer buffer = buf list . m buffers[0];ns data * PCM block =[ns data dataWithBytes:buffer . mdata length:buffer . mdatabytesize];[引擎didRecordData:PCM block];} } }返回错误;}//对于播放回调,将音频数据填充到bufferliststatic OSStatus playbackCallbackFunc(void * in refcon,audiunitrenderactionflags * ioActionFlags,const audio timestamp * inTimeStamp,UInt32 inBusNumber,UInt32 inNumberFrames,AudioBufferList * io data){ ASAudioEngineSingleU * engine =(_ _ bridge ASAudioEngineSingleU *)in refcon;OSStatus err = noErrif(engine . audiochainieingreconstructed = = NO){ for(int I = 0;我& ltio data-& gt;mNumberBuffersi++){ @ autoreleasepool { audio buffer buffer = io data-& gt;mBuffers[I];ns data * PCM block =[engine get play frame:buffer . mdatabytesize];if(PCM block & & PCM block . length){ uint 32 size =(uint 32)MIN(buffer . mdatabytesize,[PCM block length]);memcpy(buffer.mData,[pcmBlock字节],size);buffer.mDataByteSize = size//HMLogDebug(LogModuleAudio,@ & # 34;AudioUnitRender pcm数据已填充& # 34;);} else { buffer . mdatabytesize = 0;* ioActionFlags | = kAudioUnitRenderAction _ outputssilence;} }//end pool }//end for }//end ifreturn err;7.不同场景下的AudioUnit构造示例7.1 I/O No渲染从输入设备采集的数据先经过MutilChannelMixer单元,然后送到输出设备进行回放。这个构建方法是中间的单元可以调节mic采集数据的声音相位和音量。

7.2 I/O 有渲染

这种构造方法在输入输出之间增加了一个rendercallback,可以对硬件采集的数据做一些处理(比如增益、调制、音效等。)然后发送到输出进行回放。

7.3只输出和渲染适合音乐游戏和合成器的app,只使用IO单元的输出端,负责提取和整理rendercallback中的播放源,准备交付,这是一种比较简单的构造方法。

输入端有两个音频流,都是通过rendercallback的方式抓取数据,一个直接馈入混音器单元,另一个经过EQ单元处理后馈入混音器单元。

8、Tips8.1 多线程及内存管理

尽可能避免在渲染回调方法中锁定和处理耗时的操作,最大限度的提高实时性能。如果在播放或采集数据时有不同的线程读写数据,就需要对其进行锁定和保护。建议pthread相关的锁方法具有比其他锁更高的性能。音频的输入和输出通常是一个连续的过程。在收集和播放的回调中,尽可能重用缓冲区,避免缓冲区的多份拷贝,而不是每次回调都重新申请和释放,并在适当的位置添加@autoreleasepool,避免长时间运行后内存的持续增加。

8.2 格式

AudioStreamBasicDescription结构在核心音频类型中定义,音频单元和许多其他音频API都需要它进行格式配置。根据需要正确填写该结构的信息。下面是44.1k,立体声,16bit填充的例子。

audio description . msamplerate = 44100;audio description . mchannelsperframe = 2;audio description . mbitspoerchannel = 16;audio description . mframesperpacket = 1;audio description . mformatid = kaudioformatlanerpcm;audio description . mformatflags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;audio description . mbytesperframe =(audioDescription.mBitsPerChannel/8)* audio description . mchannelsperframe;audio description . mbytesperpacket = audio description . mbytesperframe;苹果官方建议,整个音频处理图或单元应尽可能以相同的音频格式流通,尽管音频单元的输入和输出可以不同。此外,单元之间的输入和输出连接点应该一致。

8.3 音质

在使用过程中,音频单元的格式可以动态改变。但在一种情况下,最好在单元被破坏前恢复默认创建时的格式,否则在单元被破坏后重建后,播放音质可能会变差(音量变低,声音粗糙)。在使用VoiceProcessing I/O Unit的过程中,在某些iphone上打开扬声器后,Unit从Mic采集的数据是空或噪音,其他从APP STORE下载的VOIP类APP也有这个问题。后来将AudioUnitSubType改为RemoteIO类型后,问题消失,怀疑苹果在VoiceProcessing Unit上处理回声消除功能时出现了bug。

8.4 AudioSession

由于使用了音频功能,因此将使用AudioSession。随着功能需求的跟进,与之相关的问题也很多,比如路由管理(听筒扬声器、线控耳机、蓝牙耳机)、中断处理(中断、iphone通话)等。这里以音频单元为主,不再赘述。应该注意的是

音频的路由变更(用户挺拔耳机,或者代码调用强制切换)涉及到iOS硬件上输入和输出设备的改变,I/O类型Unit的采集和播放线程在切换过程中会阻塞一定时间(200ms左右),如果是语音对讲类对实时性要求较高的应用场景要考虑丢包策略。在APP前台工作时,iPhone来电或者用户主动切换到其它音频类APP后,要及时处理音频的打断机制,在恰当的时机停止及恢复Unit的工作,由于iOS平台对资源的独占方式,iPhone在通话等操作时,APP中的Unit是无法初始化或者继续工作的。

原文链接:iOS音频单元(Juan)-CuO CuO

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。

发表回复

登录后才能评论