iOS 音频-audioUnit 总结

栏目: IOS · 发布时间: 6年前

内容简介:在看 LFLiveKit 代码的时候,看到音频部分使用的是 audioUnit 做的,所以把 audioUnit 学习了一下。总结起来包括几个部分:播放、录音、音频文件写入、音频文件读取.demo 放在###基本认识

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

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

###基本认识

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

iOS 音频-audioUnit 总结

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

iOS 音频-audioUnit 总结

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

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

iOS 音频-audioUnit 总结

这个图展示了一个完整的录音+混音+播放的流程,在组件两边设置 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;
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Learning JavaScript

Learning JavaScript

Shelley Powers / Oreilly & Associates Inc / 2006-10-17 / $29.99

As web browsers have become more capable and standards compliant, JavaScript has grown in prominence. JavaScript lets designers add sparkle and life to web pages, while more complex JavaScript has led......一起来看看 《Learning JavaScript》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具