音视频学习笔记(一)Audio Units

音视频学习笔记(一)Audio Units

Audio Unit Hosting Guide for iOS 官方文档

最近在学习iOS音视频开发,在此写一个学习笔记。

Programming layer in the iOS audio stack.png


前言

Audio Unit提供高效、模块化的音频处理方案。当你需要实现以下需求时,推荐直接使用 Audio Units:

  • 想使用低延迟的音频I/O(input或者output),比如说在VoIP的应用场景下;
  • 多路声音的合成并且回放,比如游戏或者音乐合成器的应用;
  • 使用AudioUnit里面提供的特有功能,比如:回声消除、Mix两轨音频,以及均衡器、压缩器、混响器等效果器;
  • 链式处理结构,你可以将音频处理模块集成到灵活的网络中(这是 iOS 中唯一提供此功能的音频 API)。

否则可以优先考虑Media Player, AV Foundation, OpenAL,或者Audio Toolbox frameworks,移步Multimedia Programming Guide 官方文档

Audio Units 提供四大类,共七种音频处理单元:(Identifier Keys for Audio Units.)

Purpose Audio units
Effect iPod Equalizer
Mixing 3D Mixer
Mixing Multichannel Mixer
I/O Remote I/O
I/O Voice-Processing I/O
I/O Generic Output
Format conversion Format Converter

构建你的APP

无论您选择哪种设计模式,构建音频单元托管应用程序的步骤基本相同:

  1. 配置 audio session;
  2. 指定audio units;
  3. 创建audio processing graph,然后获取audio units;
  4. 配置 audio units;
  5. 连接audio units节点;
  6. 提供用户界面;
  7. 初始化然后启动audio processing graph

1. AudioSession

音频会话(AudioSession),其用于管理与获取iOS设备音频的硬件信息,并且是以单例的形式存在。可以使用如下代码来获取AudioSession的实例:

AVAudioSession *audioSession = [AVAudioSession sharedInstance];

获得AudioSession的实例之后,就可以设置以何种方式使用音频硬件做哪些处理了,基本的设置具体如下所示:

// 1. 根据我们需要硬件设备提供的能力来设置类别:(AVAudioSessionCategory)
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];

//2. 设置I/O的Buffer,Buffer越小则说明延迟越低:
NSTimeInterval bufferDuration = 0.002;
[audioSession setPreferredIOBufferDuration:bufferDuration error:&error];

//3. 设置采样频率,让硬件设备按照设置的采样频率来采集或者播放音频:
double hwSampleRate = 44100.0;
[audioSession setPreferredSampleRate:hwSampleRate error:&error];

4. 当设置完毕所有的参数之后就可以激活AudioSession了,代码如下:
[audioSession setActive:YES error:&error];

2. 构建AudioUnit

构建AudioUnit的时候需要指定类型(Type)、子类型(subtype)以及厂商(Manufacture)。 如需创建一个通用描述,可以将类型或者子类型设置为0,例如为了匹配所有的 I/O unit,可以将 componentSubType 设置为0。

如果要输出音频,那么就要如下设置:

AudioComponentDescription ioUnitDescription;
/*componentType类型是相对应的,什么样的功能设置什么样的类型,componentSubType是根据componentType设置的。*/
ioUnitDescription.componentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
/*厂商的身份验证*/
ioUnitDescription.componentManufacturer=kAudioUnitManufacturer_Apple;
/*如果没有一个明确指定的值,那么它必须被设置为0*/
ioUnitDescription.componentFlags = 0;
/*如果没有一个明确指定的值,那么它必须被设置为0*/
ioUnitDescription.componentFlagsMask = 0;

上述代码构造了RemoteIO类型的AudioUnit描述的结构体,那么如何使用这个描述来构造真正的AudioUnit呢?有两种方式:第一种方式是直接使用AudioUnit裸的创建方式;第二种方式是使用AUGraph和AUNode(其实一个AUNode就是对AudioUnit的封装,可以理解为一个AudioUnit的Wrapper)来构建。

  1. 使用 audio unit API 获取 audio unit 实例:
// 1. 首先根据AudioUnit的描述,找出实际的AudioUnit类型:
AudioComponent ioUnitRef = AudioComponentFindNext(NULL, &ioUnitDescription)
// 2. 然后声明一个AudioUnit引用:
AudioUnit ioUnitInstance;
// 3. 最后根据类型创建出这个AudioUnit实例:
AudioComponentInstanceNew(ioUnitRef, &ioUnitInstance);
  1. 使用 audio processing graph API 获取 audio unit
// 1. 首先声明并且实例化一个AUGraph:
AUGraph processingGraph;
NewAUGraph (&processingGraph);

// 2. 然后按照AudioUnit的描述在AUGraph中增加一个AUNode:
AUNode ioNode;
AUGraphAddNode (processingGraph, &ioUnitDescription, &ioNode);

// 3. 接下来打开AUGraph,其实打开AUGraph的过程也是间接实例化AUGraph中所有的AUNode。注意,必须在获取AudioUnit之前打开整个AUGraph,否则我们将不能从对应的AUNode中获取正确的AudioUnit:
AUGraphOpen (processingGraph);

// 4. 最后在AUGraph中的某个Node里获得AudioUnit的引用:
AudioUnit ioUnit;
AUGraphNodeInfo (processingGraph, ioNode, NULL, &ioUnit);

附:如下图所示: componentType 和 componentSubType 根据不同的音频单元功能来设置,几种常用的如下图:

AudioComponentDescription.png

3. AudioUnit的通用参数设置

AudioUnit Struct.png

以Remote I/O为例,RemoteIO Unit分为Element0和Element1,其中Element0控制输出端,Element1控制输入端,同时每个Element又分为Input Scope和Output Scope。如果开发者想要使用扬声器的声音播放功能,那么必须将这个Unit的Element0的OutputScope和Speaker进行连接。而如果开发者想要使用麦克风的录音功能,那么必须将这个Unit的Element1的InputScope和麦克风进行连接。
注:这种结构比较常见,但这并不适合所有情况。例如在 mixer unit 中,会存在多个 input element,一个 output element的情况。

  • 使用扬声器:
OSStatus status = noErr;
UInt32 oneFlag = 1;
UInt32 busZero = 0;// Element 0
status = AudioUnitSetProperty(remoteIOUnit,
        kAudioOutputUnitProperty_EnableIO,
        kAudioUnitScope_Output,
        busZero,
        &oneFlag,
        sizeof(oneFlag));
CheckStatus(status, @"Could not Connect To Speaker", YES);
  • 启用麦克风:
UInt32 busOne = 1; // Element 1
AudioUnitSetProperty(remoteIOUnit,
        kAudioOutputUnitProperty_EnableIO,
        kAudioUnitScope_Input,
        busOne,
        &oneFlag,
        sizeof(oneFlag);

AudioUnitSetProperty 方法的说明,很简单,就不翻译了

/*!
    @function       AudioUnitSetProperty
    @abstract       sets the value of a specified property
    @discussion     The API can is used to set the value of the property. Property values for 
                    audio units are always passed by reference
                    
    @param          inUnit
                    the audio unit
    @param          inID
                    the property identifier
    @param          inScope
                    the scope of the property
    @param          inElement
                    the element of the scope
    @param          inData
                    if not null, then is the new value for the property that will be set. If null, 
                    then inDataSize should be zero, and the call is then used to remove a 
                    previously set value for a property. This removal is only valid for
                    some properties, as most properties will always have a default value if not 
                    set.
    @param          inDataSize  
                    the size of the data being provided in inData

    @result         noErr, or various audio unit errors related to properties
*/
};

下面是一些常用的属性:

  • kAudioOutputUnitProperty_EnableIO 启用或禁止 I/O,默认输出开启,输入禁止。
  • kAudioUnitProperty_ElementCount 配置元素个数
  • kAudioUnitProperty_MaximumFramesPerSlice 设置 audio unit 的最大帧数
  • kAudioUnitProperty_StreamFormat 指定输入 audio unit 输入输出元素的数据格式

再来看一下kAudioUnitProperty_StreamFormat;在iOS平台不论音频还是视频的API都会接触到很多StreamBasic Description,它是用来描述音视频具体格式的。下面就来具体分析一下上述代码是如何指定格式的。

UInt32 bytesPerSample = sizeof(Float32);
AudioStreamBasicDescription asbd;
bzero(&asbd, sizeof(asbd));
// mFormatID参数可用来指定音频的编码格式,此处指定音频的编码格式为PCM格式。
asbd.mFormatID = kAudioFormatLinearPCM;
// 声音的采样率
asbd.mSampleRate = _sampleRate;
// 声道数
asbd.mChannelsPerFrame = channels;
// 每个Packet有几个Frame
asbd.mFramesPerPacket = 1;

asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked |
                kAudioFormatFlagIsNonInterleaved;

// 一个声道的音频数据用多少位来表示
asbd.mBitsPerChannel = 8 * bytesPerSample;

/* 
最终是参数mBytesPerFrame和mBytesPerPacket的赋值,这里需要根据mFormatFlags的值来进行分配,
如果在NonInterleaved的情况下,就赋值为bytesPerSample(因为左右声道是分开存放的);
但如果是Interleaved的话,那么就应该是bytesPerSample*channels(因为左右声道是交错存放的),
这样才能表示一个Frame里面到底有多少个byte。
*/
asbd.mBytesPerFrame = (asbd.mBitsPerChannel / 8)
asbd.mBytesPerPacket =(asbd.mBitsPerChannel / 8);

其中,mFormatFlags是用来描述声音表示格式的参数,代码中的第一个参数指定每个sample的表示格式是Float格式,这点类似于之前讲解的每个sample都是使用两个字节(SInt16)来表示;然后是后面的参数kAudioFormatFlagIsNonInterleaved,字面理解这个单词的意思是非交错的,其实对于音频来讲就是左右声道是非交错存放的,实际的音频数据会存储在一个AudioBufferList结构中的变量mBuffers中,如果mFormatFlags指定的是NonInterleaved,那么左声道就会在mBuffers[0]里面,右声道就会在mBuffers[1]里面;而如果mFormatFlags指定的是Interleaved的话,那么左右声道就会交错排列在mBuffers[0]里面,理解这一点对于后续的开发将是十分重要的。

最后,调用AudioUnitSetProperty来设置给对应的AudioUnit:

AudioUnitSetProperty( remoteIOUnit, kAudioUnitProperty_StreamFormat,
        kAudioUnitScope_Output, 1, &asbd, sizeof(asbd));

参考文档:

  • 《音视频开发进阶指南》 - 展晓凯
  • Audio Unit 基础

你可能感兴趣的:(音视频学习笔记(一)Audio Units)