1音视频从入门到放弃

AudioUnit 基础知识

2020-07-05  本文已影响0人  Seacen_Liu

Overwrite

个人理解的名词解释:

Audio_frameworks_2x.png

如上图所示,AudioUnit是iOS中音频最底层的API,仅在高性能,专业处理声音的需求下使用.

1. Audio Unit 提供快速、模块化的音频处理

使用场景

1.1 Audio Unit 的用途

用途 Audio Unit
音效(Effect) iPod Equalizer
音频混合(Mixing) 3D Mixer、Multichannel Mixer
音频输入输出(I/O) Remote I/O、Voice-Processing I/O、Generic Output
格式转换(Format conversion) Format Converter

1.2 Audio Unit 的两套 API

iOS 中操控 Audio Unit 的有两套API,一套是用于直接操控单个 Audio Unit,另一套通过音频处理图(audio processing graphs)的方式操控多个Audio Unit。

AudioComponentDescription ioUnitDescription;
 
ioUnitDescription.componentType          = kAudioUnitType_Output;
ioUnitDescription.componentSubType       = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufacturer  = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags         = 0;
ioUnitDescription.componentFlagsMask     = 0;

AudioComponentDescription:一个用来描述并唯一标识一个音频组件的结构体

标识字段(Identifier keys):

更多 Identifier keys 见 Identifier Keys for Audio Units

1 - 2 获取 Audio Unit 对象实例

Audio Unit API

AudioComponent foundIoUnitReference = AudioComponentFindNext (
                                          NULL,
                                          &ioUnitDescription
                                      );
AudioUnit ioUnitInstance;
AudioComponentInstanceNew (
    foundIoUnitReference,
    &ioUnitInstance
);

AudioComponent类型:音频组件类型,本质是一个指定特定音频组件的类指针。一个单独的音频组件,可用于实例化多个相同类型的音频单元实例。

AudioComponentFindNext函数:该函数用于在系统设备可用组件中查找最相近的组件,并将其返回其类指针。可用组件的顺序可能会根据系统或者Audio Unit 的版本进行变化,详情见kAudioComponentRegistrationsChangedNotification

AudioComponentInstanceNew函数:该函数用于创建音频组件实例,即一个音频单元(audio unit)。

Audio Graph API

// 声明并实例化一个音频处理图
AUGraph processingGraph;
NewAUGraph(&processingGraph);
 
// 添加音频单元结点到图中,然后实例化这些结点
AUNode ioNode;
AUGraphAddNode (
    processingGraph,
    &ioUnitDescription,
    &ioNode
);

// 间接执行音频单元的实例化
AUGraphOpen (processingGraph); 

// 获取新实例化的 I/O 单元引用
AudioUnit ioUnit;
AUGraphNodeInfo (
    processingGraph,
    ioNode,
    NULL,
    &ioUnit
);

NewAUGraph函数:实例化一个音频处理图

AUGraphAddNode函数:通过音频组件描述添加一个音频结点到音频处理图中。

AUGraphOpen函数:打开指定的音频处理图(头文件中表示音频单元在此时并没有初始化,该操作并不会有资源被申请?)。

AUGraphNodeInfo函数:返回指定音频结点的信息。

1.4 Audio Units 的 Scopes 与 Elements

一个音频单元由以下的ScopesElements组成:

audioUnitScopes_2x.png

[图片上传中...(IO_unit_2x.png-b104de-1593881466577-0)]

1.5 Audio Units 的属性配置

一个音频单元的属性是通过一个键值对来设置的,其中键是通过一个唯一的整数(UInt32)来标识的。苹果预留了0 ~ 63999的区间标识属性键,剩下的区间可提供给第三方音频单元使用。我们可以通过AudioUnitSetProperty函数进行属性配置,调用方式由以下代码块所示。

UInt32 busCount = 2;
 
OSStatus result = AudioUnitSetProperty (
    mixerUnit,                         // 属性设置目标音频单元
    kAudioUnitProperty_ElementCount,   // 属性键
    kAudioUnitScope_Input,             // 属性设置所在域
    0,                                 // 属性设置所在元件
    &busCount,                         // 属性值地址
    sizeof(busCount)                  // 属性值的字节大小
);

常用属性

大多数属性只能在音频单元没有初始化时指定,但是某些特定属性可以在音频单元运行时设置,如``Voice-Processing I/O unitkAUVoiceIOProperty_MuteOutput静音功能、iPod EQ unitkAudioUnitProperty_PresentPreset`当前模式功能。

属性相关函数

1.6 Audio Units 的参数配置

相比与音频单元属性,音频单元参数在音频单元工作的过程中都是可以调整的。音频单元参数也是以键值对的方式表示的。

参数相关函数(其使用方式类似属性获取与设置)

我们可以通过UIKit中的UISliderUISwitch控件在音频单元工作的过程中改变其参数值,达到用户交互的效果。

1.7 I/O Units 的基本特性

I/O Units 是一个十分常用的音频单元,它在许多地方都比较特别。它包含了两个element,内部结构如下图所示:

IO_unit_2x.png

I/O Unit 的两个elements是两个独立的实体,在使用的时候我们需要通过设置属性(kAudioOutputUnitProperty_EnableIO)单独启用和禁用element

因此 I/O unit 在音频处理图中担任着音频流处理的起始点和终点,拥有开启和关闭音频流的能力。

2. Audio Processing Grapha 管理 Audio Units

Audio Processing Grapha 即一个音频处理图,是 Core-Foundation风格的数据结构——AUGrapha。通过 AUGrapha 我们可以构建和管理一个音频处理链条。一个音频处理图可以利用多个音频单元和多个呈现回调函数创建任何你所想象的音频处理方案。

总的来说,构建音频处理图需要以下三步:

  1. 添加音频结点到音频图中
  2. 通过音频结点获取音频单元,直接配置音频单元的属性参数
  3. 将音频结点连接起来

2.1 Audio Processing Graphs 中的 I/O Unit

无论是录音、回放和同步I/O流,所有音频处理图都有一个 I/O unit。音频图中的 I/O unit 负责音频流的输入和输出,其他音频单元负责其他音频流处理工作。

2.2 Audio Processing Graphs 线程安全

音频处理图API提供一个“to-do list”保存需要做的操作,在开发者配置完成后再告诉音频图去实现。以下是音频处理图支持的一些常见重新配置以及相关功能:

下面将以一个音频合成播放的音频处理图重新配置为例,讲述音频处理图运行中的线程安全。首先构建一个音频处理图包含 Multichannel Mixer unit 和 Remote I/O unit,用于播放合成两种输入源的混音效果。运行中的音频处理图中,开发者将两个输入源的数据送给 Mixer unit 的 input bus,mixer 的输出端连接着 I/O unit 的output element,最终将音频传给硬件输出。音频处理图如下:

AudioProcessingGraphBeforeEQ_2x.png

现在,在其中一个音频流中插入一个“音频均衡器”,则音频处理图如下:

AudioProcessingGraphWithEQ_2x.png

以下是完成重新配置的步骤:

  1. 通过调用AUGraphDisconnectNodeInput断开mixer unitinput 1的“鼓声”回调。
  2. 通过配置“音频组件描述”,然后调用AUGraphAddNode将一个包含一个包含iPod EQ unit的音频结点到音频图中的音频结点添加到音频图中。(此时,iPod EQ unit已具有实例化对象但未被初始化,新增的音频结点也只是存在音频图中并未参与音频流)
  3. 配置和初始化iPod EQ unit
    • 调用AudioUnitGetProperty函数从Mixer unit的输入端获取当前使用的流格式(kAudioUnitProperty_StreamFormat)
    • 调用AudioUnitSetProperty函数两次,分别设置iPod EQ unit输入端和输出端的流格式
    • 调用AudioUnitInitialize函数为iPod EQ unit分配内存和准备处理音频使用。(注:这个函数是线程不安全的,需要当iPod EQ unit尚未主动参与进音频处理图时,即没有调用AUGraphUpdate函数前使用。
  4. 通过调用AUGraphSetNodeInputCallback将“鼓声”回调函数添加到iPod EQ unitinput端。

上面1,2,4步使用AUGraph开头的函数,都会被添加到的任务执行列表(“to-do list”)中。然后通过调用AUGraphUpdate执行这些未开始任务。如果成功返回,则代表音频图已经被动态重新配置并且iPod EQ unit也已经就位正在处理音频数据。

2.3 通过 Graph "pull"音频流

在音频处理图的音频流流动类似生产者消费者模式,消费者在需要更多音频数据时通知生产者。请求音频数据流的方向与音频流提供的方向正好相反。

pull_model_2x.png

对一组音频数据的每个请求称为渲染调用(render call),也称为拉流(pull)。该图灰色“控制流”箭头表示为拉流操作。拉流请求的数据本质是一组音频样本帧(audio sample frames),一组音频样本帧也称为一个切片(slice)。提供切片的代码称为渲染回调函数( render callback function)。下列是拉取音频流的步骤:

  1. 调用AUGraphStart函数后,虚拟输出设备调用Remote I/O unitoutput element的渲染回调函数请求一片处理过的音频数据帧。
  2. Remote I/O unit的回调函数在其输入缓冲区中查找要处理的音频数据去满足渲染请求。如果有数据则直接传递,否则,调用连接其输入端的回调函数。上图中Remote I/O unit的输入端连接一个effect unit的输出端,则I/O uniteffect unit中拉流,请求一片音频数据帧。
  3. effect unit的行为与Remote I/O unit一样。当它需要音频数据时,便从输入连接中获取它。上图中,effect unit从应用程序的回调函数中获取音频数据。
  4. 应用程序的回调函数最终接收了这个拉流请求,在函数中提供effect unit需要的音频帧数据。
  5. effect unit从应用程序的回调函数中获取的音频数据,然后按照步骤2中要求提供音频数据给Remote I/O unit
  6. Remote I/O unit将从effect unit提供的音频数据,按照步骤1中的要求提供给虚拟输出设备,完成了一个拉流周期。

3. 通过回调函数将音频传递给 Audio Units

为了将音频数据从内存或磁盘中传递到音频单元的input bus,需要使用实现一个渲染回调函数填充数据并通过AURenderCallback属性配置音频单元的属性。这样当音频单元需要一片音频帧数据时候,该回调函数就会被调用。渲染回调函数给了我们操作音频数据很高的自由度,我们可以在回调函数以任何方式创建或改变音频数据。与此同时,渲染回调函数处于实时优先级线程上,后续的函数调用都是异步的。因此,我们在渲染回调函数中处理时间是有限的,如果在下一次渲染调用到达时,上一个回调函数还没运行完成,那么你的音频结果将会出现缺口,导致你的结果是不连续的。因此,不能在渲染回调函数中执行线程锁、分配内存、访问文件系统或网络连接等耗时任务。

以下是渲染回调函数的详细说明:

static OSStatus MyAURenderCallback (
    void                        *inRefCon,
    AudioUnitRenderActionFlags  *ioActionFlags,
    const AudioTimeStamp        *inTimeStamp,
    UInt32                      inBusNumber,
    UInt32                      inNumberFrames,
    AudioBufferList             *ioData
) { /* callback body */ }

下图描述的是ioData参数中的一对非交错(noninterleaved)立体声缓冲区:

ioDataBuffers_2x.png

4. 音频流格式启用数据流

在一个音频样本数据中,二进制位的布局是有含义的,这不是单纯用Float32UInt16数据类型可以表达的。音频单元(audio unit)可以使用音频组件描述(AudioComponentDescription)来表达,音频流格式则使用音频流基本描述(AudioStreamBasicDescription,即ASBD)来表达。

// ASBD 结构体
struct AudioStreamBasicDescription {
    Float64 mSampleRate;
    UInt32  mFormatID;
    UInt32  mFormatFlags;
    UInt32  mBytesPerPacket;
    UInt32  mFramesPerPacket;
    UInt32  mBytesPerFrame;
    UInt32  mChannelsPerFrame;
    UInt32  mBitsPerChannel;
    UInt32  mReserved;
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

// 定义一个立体声 ASBD
// AudioUnitSampleType => 8.24 fixed-point integer => SInt32
size_t bytesPerSample = sizeof(AudioUnitSampleType); 
AudioStreamBasicDescription stereoStreamFormat = {0};
stereoStreamFormat.mSampleRate        = graphSampleRate;
stereoStreamFormat.mFormatID          = kAudioFormatLinearPCM; // 未压缩的音频数据
/*
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsFloat |
                                kAudioFormatFlagsNativeEndian |
                                     kAudioFormatFlagIsPacked |
                             kAudioFormatFlagIsNonInterleaved
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsSignedInteger |
                                                                                kAudioFormatFlagsNativeEndian | 
                                                                                     kAudioFormatFlagIsPacked |             
                                                                         kAudioFormatFlagIsNonInterleaved | (kAudioUnitSampleFractionBits << kLinearPCMFormatFlagsSampleFractionShift)
*/
stereoStreamFormat.mFormatFlags       = kAudioFormatFlagsAudioUnitCanonical;
stereoStreamFormat.mBytesPerPacket    = bytesPerSample;
stereoStreamFormat.mFramesPerPacket   = 1;
stereoStreamFormat.mBytesPerFrame     = bytesPerSample;
stereoStreamFormat.mChannelsPerFrame  = 2; // 2 indicates stereo
stereoStreamFormat.mBitsPerChannel    = 8 * bytesPerSample;
stereoStreamFormat.mReserved          = 0;

注:音频流格式在创建的时候需要将事先初始化为0,即不包含任何数据,否则可能会出bug。

音频处理图中必须在关键点设置好音频数据格式,其他点系统将会设置自动格式。iOS 设备上的音频输入和输出硬件具有系统确定的音频流格式,该格式始终是未压缩的,采用交错的线性 PCM 格式。

IOWithoutRenderCallback_2x.png
上一篇 下一篇

猜你喜欢

热点阅读