iOS 音频-audioUnit 总结

在看 LFLiveKit 代码的时候,看到音频部分使用的是 audioUnit 作的,因此把 audioUnit 学习了一下。总结起来包括几个部分:播放、录音、音频文件写入、音频文件读取.html

demo 放在VideoGather这个库,里面的 audioUnitTest 是各个功能的测试研究、singASong 是集合各类音频处理组件来作的一个“播放伴奏+唱歌 ==> 混音合成歌曲”的功能。git

###基本认识github

AudioUnitHostingFundamentals这个官方文档里有几个不错的图:bash

audioUnitScopes_2x.png

对于通用的audioUnit,能够有1-2条输入输出流,输入和输出不必定相等,好比mixer,能够两个音频输入,混音合成一个音频流输出。每一个element表示一个音频处理上下文(context), 也称为bus。每一个element有输出和输出部分,称为 scope,分别是 input scope 和 Output scope。Global scope 肯定只有一个 element,就是 element0,有些属性只能在 Global scope 上设置。session

IO_unit_2x (1).png

对于 remote_IO 类型 audioUnit,即从硬件采集和输出到硬件的 audioUnit,它的逻辑是固定的:固定 2 个 element,麦克风通过 element1 到 APP,APP 经 element0 到扬声器。app

咱们能把控的是中间的“APP 内处理”部分,结合上图,淡黄色的部分就是APP可控的,Element1 这个组件负责连接麦克风和 APP,它的输入部分是系统控制,输出部分是APP控制;Element0 负责链接 APP 和扬声器,输入部分 APP 控制,输出部分系统控制。ide

IOWithoutRenderCallback_2x (1).png

这个图展现了一个完整的录音+混音+播放的流程,在组件两边设置 stream 的格式,在代码里的概念是 scope。函数

文件读取

demo 在 TFAudioUnitPlayer 这个类,播放须要音频文件读取和输出的 audioUnit。性能

文件读取使用 ExtAudioFile,这个据我了解,有两点很重要:1.自带转码 2.只处理 pcm。学习

不只是 ExtAudioFile,包括其余 audioUnit,其实应该是流数据处理的性质,这些组件都是“输入+输出”的这种工做模式,这种模式决定了你要设置输出格式、输出格式等。

  • ExtAudioFileOpenURL使用文件地址构建一个 ExtAudioFile 文件里的音频格式是保存在文件里的,不用设置,反而能够读取出来,好比获得采样率用做后续的处理。

  • 设置输出格式

AudioStreamBasicDescription clientDesc;
   clientDesc.mSampleRate = fileDesc.mSampleRate;
   clientDesc.mFormatID = kAudioFormatLinearPCM;
   clientDesc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
   clientDesc.mReserved = 0;
   clientDesc.mChannelsPerFrame = 1; //2
   clientDesc.mBitsPerChannel = 16;
   clientDesc.mFramesPerPacket = 1;
   clientDesc.mBytesPerFrame = clientDesc.mChannelsPerFrame * clientDesc.mBitsPerChannel / 8;
   clientDesc.mBytesPerPacket = clientDesc.mBytesPerFrame;
复制代码

pcm是没有编码、没有压缩的格式,更方便处理,因此输出这种格式。首先格式用 AudioStreamBasicDescription 这个结构体描述,这里包含了音频相关的知识:

  • 采样率 SampleRate: 每秒钟采样的次数

  • 帧 frame:每一次采样的数据对应一帧

  • 声道数 mChannelsPerFrame:人的两个耳朵对统一音源的感觉不一样带来距离定位,多声道也是为了立体感,每一个声道有单独的采样数据,因此多一个声道就多一批的数据。

  • 最后是每一次采样单个声道的数据格式:由 mFormatFlags 和 mBitsPerChannel 肯定。mBitsPerChannel 是数据大小,即采样位深,越大取值范围就更大,不容易数据溢出。mFormatFlags 里包含是否有符号、整数或浮点数、大端或是小端等。有符号数就有正负之分,声音也是波,振动有正负之分。这里采用 s16 格式,即有符号的 16 比特整数格式。

  • 从上至下是一个包含关系:每秒有 SampleRate 次采样,每次采样一个 frame,每一个 frame有mChannelsPerFrame 个样本,每一个样本有 mBitsPerChannel 这么多数据。因此其余的数据大小均可以用以上这些来计算获得。固然前提是数据时没有编码压缩的

  • 设置格式:

size = sizeof(clientDesc);
   status = ExtAudioFileSetProperty(audioFile, kExtAudioFileProperty_ClientDataFormat, size, &clientDesc);
复制代码

在APP这一端的是 client,在文件那一端的是 file,带 client 表明设置 APP 端的属性。测试 mp3 文件的读取,是能够改变采样率的,即mp3文件采样率是 11025,能够直接读取输出 44100 的采样率数据。

  • 读取数据 ExtAudioFileRead(audioFile, framesNum, bufferList) framesNum 输入时是想要读取的 frame 数,输出时是实际读取的个数,数据输出到 bufferList 里。bufferList 里面的 AudioBuffer 的 mData 须要分配内存。

播放

播放使用 AudioUnit,首先由3个相关的东西:AudioComponentDescription、AudioComponent 和 AudioComponentInstance。AudioUnit 和 AudioComponentInstance是一个东西,typedef 定义的别名而已。

AudioComponentDescription 是描述,用来作组件的筛选条件,相似于 SQL 语句 where 以后的东西。

AudioComponent 是组件的抽象,就像类的概念,使用AudioComponentFindNext来寻找一个匹配条件的组件。

AudioComponentInstance 是组件,就像对象的概念,使用 AudioComponentInstanceNew 构建。

构建了 audioUnit 后,设置属性:

  • kAudioOutputUnitProperty_EnableIO,打开 IO。默认状况 element0,也就是从 APP 到扬声器的IO时打开的,而 element1,即从麦克风到 APP 的 IO 是关闭的。使用 AudioUnitSetProperty 函数设置属性,它的几个参数分别做用是:
    • 1.要设置的 audioUnit
    • 2.属性名称
    • 3.element, element0 和 element1 选一个,看你是接收音频仍是播放
    • 4.scope 也就是范围,这里是播放,咱们要打开的是输出到系统的通道,使用 kAudioUnitScope_Output
    • 5.要设置的值
    • 6.值的大小。

比较难搞的就是 element 和 scope,须要理解 audioUnit 的工做模式,也就是最开始的两张图。

  • 设置输入格式AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, renderAudioElement, &audioDesc, sizeof(audioDesc));,格式就用 AudioStreamBasicDescription 结构体数据。输出部分是系统控制,因此不用管。

  • 而后是设置怎么提供数据。这里的工做原理是:audioUnit 开启后,系统播放一段音频数据,一个 audioBuffer,播完了,经过回调来跟 APP 索要下一段数据,这样循环,知道你关闭这个 audioUnit。重点就是:

    • 1.是系统主动来跟你索要,不是咱们的程序去推送数据
    • 2.经过回调函数。就像 APP 这边是工厂,而系统是商店,他们断货了或者要断货了,就来跟咱们进货,直到你工厂倒闭了、不卖了等等

因此设置播放的回调函数:

AURenderCallbackStruct callbackSt;
   callbackSt.inputProcRefCon = (__bridge void * _Nullable)(self);
   callbackSt.inputProc = playAudioBufferCallback;
AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Group, renderAudioElement, &callbackSt, sizeof(callbackSt));
复制代码

传入的数据类型是 AURenderCallbackStruct 结构体,它的inputProc 是回调函数,inputProcRefCon 是回调函数调用时,传递给 inRefCon 的参数,这是回调模式经常使用的设计,在其余地方可能叫 context。这里把 self 传进去,就能够拿到当前播放器对象,获取音频数据等。

回调函数

回调函数里最主要的目的就是给 ioData 赋值,把你想要播放的音频数据填入到 ioData 这个 AudioBufferList 里。结合上面的音频文件读取,使用 ExtAudioFileRead 读取数据就能够实现音频文件的播放。

播放功能自己是不依赖数据源的,由于使用的是回调函数,因此文件或者远程数据流均可以播放。

录音

录音类 TFAudioRecorder,文件写入类 TFAudioFileWriter 和 TFAACFileWriter。为了更自由的组合音频处理的组件,定义了 TFAudioOutput 类和 TFAudioInput 协议,TFAudioOutput 定义了一些方法输出数据,而 TFAudioInput 接受数据。

在 TFAudioUnitRecordViewController 类的 setupRecorder 方法里设置了4种测试:

  • pcm 流写入到 caf 文件
  • pcm 经过 extAudioFile 写入,extAudioFile 内部转换成aac格式,写入 m4a 文件
  • pcm 转 aac 流,写入到 adts 文件
  • 比较 2 和 3 两种方式性能
1. 使用audioUnit获取录音数据

和播放时同样,构建 AudioComponentDescription 变量,使用AudioComponentFindNext寻找 audioComponent,再使用 AudioComponentInstanceNew 构建一个 audioUnit。

  • 开启 IO:
UInt32 flag = 1;
   status = AudioUnitSetProperty(audioUnit,kAudioOutputUnitProperty_EnableIO, // use io
                                 kAudioUnitScope_Input, // 开启输入
                                 kInputBus, //element1是硬件到APP的组件
                                 &flag, // 开启,输出YES
                                 sizeof(flag));
复制代码

element1是系统硬件输入到APP的element,传入值1标识开启。

  • 设置输出格式:
AudioStreamBasicDescription audioFormat;
   audioFormat = [self audioDescForType:encodeType];
   status = AudioUnitSetProperty(audioUnit,
                                 kAudioUnitProperty_StreamFormat,
                                 kAudioUnitScope_Output,
                                 kInputBus,
                                 &audioFormat,
                                 sizeof(audioFormat));
复制代码

audioDescForType 这个方法里,只处理了AAC和PCM两种格式,pcm的时候能够本身计算,也能够利用系统提供的一个函数 FillOutASBDForLPCM 计算,逻辑是跟上面的说的同样,理解音频里的采样率、声道、采样位数等关系就好搞了。

对 AAC 格式,由于是编码压缩了的,AAC 固定 1024frame 编码成一个包(packet),许多属性没有用了,好比 mBytesPerFrame,但必须把他们设为0,不然未定义的值可能形成影响

  • 设置输入的回调函数
AURenderCallbackStruct callbackStruct;
   callbackStruct.inputProc = recordingCallback;
   callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
   status = AudioUnitSetProperty(audioUnit,kAudioOutputUnitProperty_SetInputCallback,
                                 kAudioUnitScope_Global,
                                 kInputBus,
                                 &callbackStruct,
                                 sizeof(callbackStruct));
复制代码

属性kAudioOutputUnitProperty_SetInputCallback指定输入的回调,kInputBus 为 1,表示 element1。

  • 开启 AVAudioSession
AVAudioSession *session = [AVAudioSession sharedInstance];
   [session setPreferredSampleRate:44100 error:&error];
   [session setCategory:AVAudioSessionCategoryRecord withOptions:AVAudioSessionCategoryOptionDuckOthers
                  error:&error];
[session setActive:YES error:&error];
复制代码

AVAudioSessionCategoryRecord 或 AVAudioSessionCategoryPlayAndRecord 均可以,后一种能够边播边录,好比录歌的APP,播放伴奏同时录制人声。

  • 最后,使用回调函数获取音频数据

构建 AudioBufferList,而后使用 AudioUnitRender 获取数据。AudioBufferList 的内存数据须要咱们本身分配,因此须要计算 buffer 的大小,根据传入的样本数和声道数来计算。

2.pcm数据写入 caf 文件

TFAudioFileWriter 类里,使用 extAudioFile 来作音频数据的写入。首先要配置 extAudioFile:

  • 构建
OSStatus status = ExtAudioFileCreateWithURL((__bridge CFURLRef _Nonnull)(recordFilePath),_fileType, &_audioDesc, NULL, kAudioFileFlags_EraseFile, &mAudioFileRef);
复制代码

参数分别是:文件地址、类型、音频格式、辅助设置(这里是移除就文件)、audioFile 变量。

这里 _audioDesc 是使用-(void)setAudioDesc:(AudioStreamBasicDescription)audioDesc从外界传入的,是上面的录音的输出数据格式。

  • 写入
OSStatus status = ExtAudioFileWrite(mAudioFileRef, _bufferData->inNumberFrames, &_bufferData->bufferList);
复制代码

在接收到音频的数据后,不断的写入,格式须要 AudioBufferList,中间参数是写入的 frame 个数。frame 和 audioDesc 里面的 sampleRate 共同影响音频的时长计算,frame 传错,时长计算就出错了。

3. 使用ExtAudioFile自带转换器来录制aac编码的音频文件

从录制的 audioUnit 输出pcm数据,测试是能够直接输入给 ExtAudioFile 来录制 AAC 编码的音频文件。在构建 ExtAudioFile 的时候设置好格式:

AudioStreamBasicDescription outputDesc;
            outputDesc.mFormatID = kAudioFormatMPEG4AAC;
            outputDesc.mFormatFlags = kMPEG4Object_AAC_Main;
            outputDesc.mChannelsPerFrame = _audioDesc.mChannelsPerFrame;
            outputDesc.mSampleRate = _audioDesc.mSampleRate;
            outputDesc.mFramesPerPacket = 1024;
            outputDesc.mBytesPerFrame = 0;
            outputDesc.mBytesPerPacket = 0;
            outputDesc.mBitsPerChannel = 0;
            outputDesc.mReserved = 0;

复制代码

重点 是mFormatID和mFormatFlags,还有个坑是那些没用的属性没有重置为0。

而后建立ExtAudioFile: OSStatus status = ExtAudioFileCreateWithURL((__bridge CFURLRef _Nonnull)(recordFilePath),_fileType, &outputDesc, NULL, kAudioFileFlags_EraseFile, &mAudioFileRef);

设置输入的格式: ExtAudioFileSetProperty(mAudioFileRef, kExtAudioFileProperty_ClientDataFormat, sizeof(_audioDesc), &_audioDesc);

其余的不变,和写入pcm同样使用 ExtAudioFileWrite 循环写入,只是须要在结束后调用 ExtAudioFileDispose 来标识写入结束,可能跟文件格式有关。

4. pcm 编码 AAC

使用 AudioConverter 来处理,demo 写在 TFAudioConvertor 类里了。

  • 构建

OSStatus status = AudioConverterNew(&sourceDesc, &_outputDesc, &_audioConverter);

和其余组件同样,须要配置输入和输出的数据格式,输入的就是录音 audiounit输出的 pcm 格式,输出但愿转化为 aac,则把 mFormatID 设为 kAudioFormatMPEG4AAC,mFramesPerPacket 设为 1024。而后采样率 mSampleRate 和声道数 mChannelsPerFrame 设一下,其余的都设为 0 就好。为了简便,采样率和声道数能够设为和输入的pcm数据同样。

编码以后数据压缩,因此输出大小是未知的,经过属性 kAudioConverterPropertyMaximumOutputPacketSize 获取输出的 packet 大小,依靠这个给输出 buffer 申请合适的内存大小。

  • 输入和转化

首先要肯定每次转换的数据大小:bufferLengthPerConvert = audioDesc.mBytesPerFrame*_outputDesc.mFramesPerPacket*PACKET_PER_CONVERT;

即每一个 frame 的大小 *每一个 packet 的 frame 数 * 每次转换的 pcket 数目。每次转换后多个 frame打包成一个 packet,因此 frame 数量最好是 mFramesPerPacket 的倍数。

receiveNewAudioBuffers 方法里,不断接受音频数据输入,由于每次接收的数目跟你转码的数目不必定相同,甚至不是倍数关系,因此一次输入可能有屡次转码,也可能屡次输入才有一次转码,还要考虑上次输入后遗留的数据等。

因此:

  1. leftLength记录上次输入转码后遗留的数据长度,leftBuf 保留上次的遗留数据

  2. 每次输入,先合并上次遗留的数据,而后进入循环每次转换 bufferLengthPerConvert 长度的数据,直到剩余的不足,把它们保存到 leftBuf进行下一次处理

转换函数自己很简单:AudioConverterFillComplexBuffer(_audioConverter, convertDataProc, &encodeBuffer, &packetPerConvert, &outputBuffers, NULL);

参数分别是:转换器、回调函数、回调函数参数 inUserData 的值、转换的 packet 大小、输出的数据。

数据输入是在会掉函数里处理,这里输入数据就经过"回调函数参数 inUserData 的值"传递进去,也能够在回调里再读取数据。

OSStatus convertDataProc(AudioConverterRef inAudioConverter,UInt32 *ioNumberDataPackets,AudioBufferList *ioData,AudioStreamPacketDescription **outDataPacketDescription,void *inUserData){
    
    AudioBuffer *buffer = (AudioBuffer *)inUserData;
    
    ioData->mBuffers[0].mNumberChannels = buffer->mNumberChannels;
    ioData->mBuffers[0].mData = buffer->mData;
    ioData->mBuffers[0].mDataByteSize = buffer->mDataByteSize;
    return noErr;
}
复制代码
相关文章
相关标签/搜索