iOS Audio Unit 实践

在iOS平台上,所有的音频框架都是基于AudioUnit实现的,使用AudioUnit会给你带来最大的自由度。

iOS平台音频框架层次图

使用场景

  • Simultaneous audio I/O (input and output) with low latency, such as for a VoIP (Voice over Internet Protocol) application
  • 低延迟的音频I/O场景,比如VoIP(俗称网络电话)
  • Responsive playback of synthesized sounds, such as for musical games or synthesized musical instruments
  • 多路声音合成回放,比如游戏、音乐合成器
  • Use of a specific audio unit feature such as acoustic echo cancelation, mixing, or tonal equalization
  • 使用AudioUnit特有功能,比如回声消除、混合、均衡器等
  • A processing-chain architecture that lets you assemble audio processing modules into flexible networks. This is the only audio API in iOS offering this capability.
  • 想使用音频处理模块组成一个处理链网络(一种图状的数据结构),在iOS平台中AudioUnit是唯一提供这种功能的API

理论

AudioUnit类型

  1. Effect

    kAudioUnitType_Effect 用于声音特效处理

    常用子类型如下:

    • 均衡效果器:kAudioUnitSubType_NBandEQ
    • 压缩效果器:kAudioUnitSubType_DynamicsProcessor
    • 混响效果器:kAudioUnitSubType_Reverb2
    • 高通:kAudioUnitSubType_HighPassFilter
    • 低通:kAudioUnitSubType_LowPassFilter
    • 带通:kAudioUnitSubType_BandPassFilter
    • 延迟:kAudioUnitSubType_Delay
    • 压限:kAudioUnitSubType_PeakLimiter
  2. Mixing

    kAudioUnitType_Mixer 用于合成多路音频流

    常用子类型:

    • MultiChannelMixer:kAudioUnitSubType_MultiChannelMixer 混合多路声音的效果器,接收多路声音的输入,可以单独调整每一路声音的增益与开关,将多路声音混合成一路输出。
  3. I/O

    kAudioUnitType_Output 提供I/O功能

    常用子类型:

    • RemoteIO:kAudioUnitSubType_RemoteIO 用来采集音频与播放音频,使用麦克风和扬声器。
    • Generic Output:kAudioUnitSubType_GenericOutput 离线处理,不需要使用扬声器播放,将音频数据放入内存队列或文件中时使用此类型。
  4. Format conversion

    kAudioUnitType_FormatConverter 用于格式转换功能,如采样格式FloatSInt16 、交错和平铺格式转换、单双声道转换。

    常用子类型:

    • AUConverter:kAudioUnitSubType_AUConverter 格式转换效果器
    • Time Pitch:kAudioUnitSubType_TimePitch 变速变调效果器
  5. Generator Unit

    kAudioUnitType_Generator 常用它来提供播放器功能

    常用子类型:

    • AudioFilePlayer:kAudioUnitSubType_AudioFilePlayer 如果输入不是麦克风是一个媒体文件使用此类型。

AudioUnit概念

通用的AudioUnit结构

一个AudioUnit包含1~2条ElementElement是音频数据处理的上下文,也称为Bus。每一个Element分为输入和输出两部分,分别称为Input scopeOutput scope

官网I/OUnit数据处理流程

官网提供的I/O类型的AudioUnit的数据处理流程图。I/O Unit将从麦克风收集来的音频数据通过Element 1Input scope输入经过Element 1Output scope输出到我们的App中。经过我们的处理逻辑后在通过Element 0Input scope输入,最后通过Element 0Output scope输出到扬声器中。

一个完整音频处理流程

一个将多个AudioUnit连接起来处理音频的流程图

播放音频实践

创建AudioUnit

要创建AudioUnit首先要创建一个AudioUnit描述结构体AudioComponentDescription 构造方法如下

1
2
public init()
public init(componentType: OSType, componentSubType: OSType, componentManufacturer: OSType, componentFlags: UInt32, componentFlagsMask: UInt32)
  • componentType:AudioUnit的类型, 例如:kAudioUnitType_Output
  • componentSubType:AudioUnit的子类型,例如:kAudioUnitSubType_RemoteIO
  • componentManufacturer:厂商 直接写 kAudioUnitManufacturer_Apple就可以了
  • componentFlags:文档中写明必须为0
  • componentFlagsMask: 同样必须为0

创建好AudioComponentDescription结构体就可以使用它来创建AudioUnit了,创建AudioUnit有两种方式:

  1. 直接创建
    1
    2
    3
    4
    //使用AudioComponentFindNext方法根据AudioComponentDescription中的描述取得符合条件的AudioComponent对象
    let component = AudioComponentFindNext(nil, &componentDesc)
    //根据这个AudioComponent对象创建出AudioUnit
    AudioComponentInstanceNew(component, &audioUnit)
  2. 使用AUGraph创建
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //首先创建一个AUGraph
    var playerGraph: AUGraph
    NewAUGraph(&playerGraph)
    //使用AudioComponentDescription的描述在AUGraph对象中添加一个AUNode
    AUGraphAddNode(playerGraph, &playerDesc, &playerNode)
    //打开AUGraph 这个方法就会实例化其中添加的所有AUNode,然后根据AUNode取得AudioUnit
    AUGraphOpen(playerGraph)
    //从playerNode中取得playerUnit实例
    AUGraphNodeInfo(playerGraph, playerNode, nil, &playerUnit)

设置AudioUnit的参数

拿到AudioUnit实例后就可以这是AudioUnit中的参数,下面是以一个Remote I/O类型的AudioUnit为例的参数设置代码:

1
2
3
4
5
6
7
let busZero: UInt32 = 0 // Element 0
var oneFlag: UInt32 = 1
//audioUnit是RemoteIO类型的AudioUnit 可以参考上面的图 连接扬声器
AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, busZero, &oneFlag, UInt32(MemoryLayout<UInt32>.size))
//连接麦克风
let busOne: Uint32 = 1 // Element 1
AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, busOne, &oneFlag, UInt32(MemoryLayout<UInt32>.size))

创建AudioStreamBasicDescription

这是一个音频数据的描述结构体,可以将这个描述设置到AudioUnit的输入或输出上,因为AudioUnit可以是多个连接在一起的,一个AudioUnit的输入是另一个AudioUnit的输出,所以分为输入和输出两部分。

1
2
3
4
5
6
7
8
9
10
let bytesPerSample: UInt32 = UInt32(MemoryLayout<Float32>.size)
var clientDesc = AudioStreamBasicDescription(mSampleRate: 44100,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved,
mBytesPerPacket: bytesPerSample,
mFramesPerPacket: 1,
mBytesPerFrame: bytesPerSample,
mChannelsPerFrame: 2,
mBitsPerChannel: 8 * bytesPerSample,
mReserved: 0)
  • mSampleRate:采样率
  • mFormatFlags:指定每个采样的数据格式,这里设置为FloatkAudioFormatFlagIsNonInterleaved表示非交错存储,音频数据实际存储在AudioBufferList结构中的mBuffers中,如果是非交错存储,左右声道音频数据就会分别存储在mBuffers[0]mBuffers[1]中,如果是Interleaved交错存储,那么左右声道的音频数据就会交错存储在mBuffers[0]中。
  • mBytesPerPacket:根据mFormatFlags指定的Float类型非交错存储,就设置为bytesPerSample表示每个采样的字节数。但如果是Interleaved交错存储的,就应该设置为bytesPerSample * mChannelsPerFrame 因为左右声道数据是交错存在一起的。
  • mBytesPerFrame:同mBytesPerPacket
  • mBitsPerChannel:表示每个声道的音频数据要多少位,一个字节是8位,所以用8 * 每个采样的字节数

接下来就可以将AudioStreamBasicDescription设置给AudioUnit

1
2
//varAudioDesc是一个AudioStreamBasicDescription的实例设置给了audioUnit的Element0的Input输入端
AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, busZero, &varAudioDesc, UInt32(MemoryLayout.size(ofValue: varAudioDesc)))

回调结构AURenderCallbackStruct

AudioUnit启动后,就会调用AURenderCallbackStruct结构中指定的函数,取得数据

1
2
3
4
5
6
7
8
9
var callbackStruct = AURenderCallbackStruct(inputProc: { (inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData) -> OSStatus in
let cls:AudioUnitPlayMusicExample = Unmanaged<AudioUnitPlayMusicExample>.fromOpaque(inRefCon).takeUnretainedValue()
var framesPerPacket = inNumberFrames
return cls.readFrame(frameNum: &framesPerPacket, bufferList: ioData)
}, inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
//将回调函数设置到AudioUnit中
AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Group, busZero, &callbackStruct, UInt32(MemoryLayout.size(ofValue: callbackStruct)))
//启动AudioUnit
status = AudioOutputUnitStart(audioUnit)

AURenderCallbackStruct有两个参数,一个是AURenderCallback的函数指针,在Swift中使用一个闭包传递进去,因为C的函数指针不允许捕获外部对象,不能使用self调用方法,所以AURenderCallbackStruct第二个参数可以把self的指针传递到AURenderCallback函数中,以备使用。

后记

本文Demo:https://github.com/zhaofucheng1129/GreatApp