iOS音频(2)——Audio Unit

一、Audio Unit综述
  1.1、Audio Unit 概念点
  1.2、 AuidoUnit类型
二、构建Audio Unit的流程
  2.1 、配置AudioSession
  2.2、指定 Audio Units类型
  2.3、创建AudioUnit
  2.4、设置AudioUnit的属性
三、数据处理
  3.1、 AURenderCallbackStruct
  3.2、串连的Audio node
  3.3、数据的转换
四、附录 
  4.1、Audio Unit 示例

一、Audio Unit综述

相对于MacOS,Audio Unit在iOS上使用到的几率很小,AV Foundation 和Audio Toolbox提供的API已经满足我们平常开发中音视频的录制播放的需求点。Audio Unit几乎可以认为是对硬件驱动层的封装,通过它获取麦克风采集的音频数据或者将音频数据传输给扬声器播放。但是随着直播热对音视频的传输速度高要求,将PCM音频转换成AAC主要用到就是Audio Unit。



  与AV Foundation 和Audio Toolbox相比较,Audio Unit主要有两大优势:
(1)时效性高,Audio Unit是接近硬件层导致对音频流的采集回调更加迅速。
(2)动态的配置,AUGraph可以动态的对音频数据的组合配置,改变音效。

1.1Audio Unit 概念点:

Audio Unit 主要涉及到三个常用的概念知识:
(1)AUGraph:包含和管理Audio Unit 的组织者;
(2)AUNode /AudioComponent:是AUGraph音频处理环节中的一个节点。
(3)AudioUnit: 音频处理组件,是对音频处理节点的实例描述者和操控者。
  我们不妨想像演唱会的舞台上,有录制歌声与乐器的麦克风,而从麦克风到输出到音响之间,还串接了大大小小的效果器,在这个过程中,无论是麦克风、音响或是效果器,都是不同的AUNode。AUNode 是这些器材的实体,而我们要操控这些器材、改变这些器材的效果属性,就会需要透过每个器材各自的操控界面,这些介面便是AudioUnit,最后构成整个舞台,便是AUGraph。AUNode 与AudioComponent 的差别在于,其实像上面讲到的各种器材,除了可以放在AUGraph 使用之外,也可以单独使用,比方说我们有台音响,我们除了把音响放在舞台上使用外,也可以单独拿这台音响输出音乐。当我们要在AUGraph 中使用某个器材,我们就要使用AUNode 这种形态,单独使用时,就使用AudioComponent。但无论是操作AUNode 或AudioComponent,都还是得透过AudioUnit 这一层操作界面。(上述文字摘自KKBOX iOS/Mac OS X 基礎開發教材)

下图所示两路音频数据首先经过均衡器单元,然后再经过混音单元组合在一起, 最后经由输入输出单元传输到到扬声器。


1.2 AuidoUnit类型

iOS提供了四大类别7种不同的AuidoUnit
AudioComponentDescription对象来描述一个具体的AudioUnit:

typedef struct AudioComponentDescription {
   OSType              componentType;
   OSType              componentSubType;
   OSType              componentManufacturer;
   UInt32              componentFlags;
   UInt32              componentFlagsMask;
} AudioComponentDescription;
  • componentType AuidoUnit主要有四种大类型:均衡器/混音/输入输出/格式转换;
  • componentSubType 指的四大类型对应的子类型 可以对照下面的表;
  • componentManufacturer 目前iOS开发中只有: kAudioUnitManufacturer_Apple;
  • componentFlags和“componentFlagsMask一般设置为0。
类型 componentType kAudioUnitType_Effect
均衡器 kAudioUnitType_Effect kAudioUnitSubType_AUiPodEQ
混音 kAudioUnitType_Mixer kAudioUnitSubType_AU3DMixerEmbedded kAudioUnitSubType_MultiChannelMixer多路 混音
输入输出 kAudioUnitType_Output kAudioUnitSubType_RemoteIO远端kAudioUnitSubType_VoiceProcessingIO kAudioUnitSubType_GenericOutput
格式转换 kAudioUnitType_FormatConverter kAudioUnitSubType_AUConverter

二、构建Audio Unit的流程

2.1 配置AudioSession

AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setPreferredSampleRate:44100 error:&error];
[audioSession setPreferredInputNumberOfChannels:1 error:&error];
[audioSession setPreferredIOBufferDuration:0.05 error:&error];
[audioSession setActive: YES error: nil];

2.2、指定 Audio Units类型

  // multichannel mixer unit
    AudioComponentDescription mixer_desc;
    mixer_desc.componentType = kAudioUnitType_Mixer;
    mixer_desc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixer_desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixer_desc.componentFlags = 0;
    mixer_desc.componentFlagsMask = 0;
    // multichannel mixer unit
    AudioComponentDescription eq_desc;
    eq_desc.componentType = kAudioUnitType_Effect;
    eq_desc.componentSubType = kAudioUnitSubType_AUiPodEQ;
    eq_desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    eq_desc.componentFlags = 0;
    eq_desc.componentFlagsMask = 0;
    // output unit
    AudioComponentDescription output_desc;
    output_desc.componentType = kAudioUnitType_Output;
    output_desc.componentSubType = kAudioUnitSubType_RemoteIO;
    output_desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    output_desc.componentFlags = 0;
    output_desc.componentFlagsMask = 0;

2.3、创建AudioUnit

(1)通过AudioComponent创建

    AudioComponent outComponent = AudioComponentFindNext(NULL,&output_desc);
    AudioUnit outUnit;
    AudioComponentInstanceNew(outComponent, &outUnit);

AudioComponentFindNext参数inComponent一般设置为NULL,从系统中找到第一个符合inDesc描述的Component,如果为其赋值,则从其之后进行寻找。
函数AudioComponentInstanceNew的第二个参数类型是AudioComponentInstance,AudioUnit实际上就是 AudioComponentInstance,在AudioComponent类中有定义:

typedef AudioComponentInstance AudioUnit;

(2)通过AUNode创建AudioUnit
AUGraph是由AUNode的串联而成,首先需要先创建一个 AUGraph:

  OSStatus status = NewAUGraph(&audioGraph);

获取一个 AUNode,第一个参数是创建的AUGraph。第二个参数是描述信息AudioComponentDescription,输出AUNode。

   AUNode mixerNode;
    status = AUGraphAddNode(audioGraph, &mixerUnitDescription, &mixerNode);

获取一个 AudioUnit,第一个和第三个参数是还是之前的AUGraph,AudioComponentDescription。第二个参数是我们刚才的AUNode,最终输出的AudioUnit。

 status = AUGraphNodeInfo(audioGraph, mixerNode, &mixerUnitDescription, &mixerUnit);

2.4、设置AudioUnit的属性

image.png

AudioUnit实际上就是一个AudioComponentInstance实例对象,一个AudioUnit由scope(范围)和element(元素)组成,实际上开发中主要涉及到输入输出的问题

CF_ENUM(AudioUnitScope) {
    kAudioUnitScope_Global      = 0,
    kAudioUnitScope_Input         = 1,
    kAudioUnitScope_Output      = 2,
    kAudioUnitScope_Group       = 3,
    kAudioUnitScope_Part        = 4,
    kAudioUnitScope_Note        = 5,
    kAudioUnitScope_Layer       = 6,
    kAudioUnitScope_LayerItem   = 7
};

scope主要使用到的输入kAudioUnitScope_Input和输出kAudioUnitScope_Output,而在element, Input用“1”(和I很像)表示,Output用“0”(和O很像)表示,

image.png
AudioUnitSetProperty(               AudioUnit               inUnit,
                                    AudioUnitPropertyID     inID,
                                    AudioUnitScope          inScope,
                                    AudioUnitElement        inElement,
                                    const void * __nullable inData,
                                    UInt32                  inDataSize) 
  • AudioUnitPropertyID 设置属性名称
  • AudioUnitScope AudioUnit的Scope 主要用于输入输出范围
  • AudioUnitElement AudioUnit的Element 主要用1 输入总线(bus),0输出总线(bus);
  • inData 输入值
  • inDataSize 输入值的长度

AudioUnit 的Remote IO有2个element,大部分代码和文献都用bus代替element,两者同义,bus0就是输出bus 1代表输入,播放音频文件就是在bus 0传送数据,bus 1输入在Remote IO 默认是关闭的,在录音的状态下 需要把bus 1设置成开启状态。

   UInt32 one = 1;
    AudioUnitSetProperty(outputUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, sizeof(one));

三、数据处理

3.1、AURenderCallbackStruct

在Audio Unit存储输入的数据和提供播放输出数据都是通过RenderCallback函数,通过AudioUnitSetProperty与输入输出回调相关联。

    AURenderCallbackStruct callBackStruct;
    callBackStruct.inputProc = BXAURenderCallback;
    callBackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
    AudioUnitSetProperty(_outAudioUinit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, 0, &callBackStruct, sizeof(AURenderCallbackStruct));

AURenderCallbackStruct是结构体,inputProc 就是我们注册回调函数。inputProcRefCon注册者。

typedef struct AURenderCallbackStruct {
    AURenderCallback __nullable inputProc;
    void * __nullable           inputProcRefCon;
} AURenderCallbackStruct;

typedef OSStatus(*AURenderCallback)(void *                          inRefCon,
                        AudioUnitRenderActionFlags *    ioActionFlags,
                        const AudioTimeStamp *          inTimeStamp,
                        UInt32                          inBusNumber,
                        UInt32                          inNumberFrames,
                        AudioBufferList * __nullable    ioData);

  • ioData 需要填充的缓存数据
  • inNumberFrames 需要填充的数据帧数,根据这个帧数 从原始音频数据格式中输出多少frame的LPCM.
  • ioActionFlags 数据回调发送错误或者其他情况 上下文传递数据。
    *inBusNumber 是输出或者输出的哪个bus.

3.2、串连的Audio node

image.png

AUGraph 中的各个Audio unit 是传连接的 需要把各个unit 通过AUGraphConnectNodeInput 连接起来 一般主要用于混音 或者音效改变的时候 需要用到。

AUGraphConnectNodeInput(_audioGraph, EQNode, 0, outNode, 0);

3.3、数据的转换

AudioConverterRef 第一个参数是输入的格式 第二个是需要输出的转换格式。

extern OSStatus
AudioConverterNew(      const AudioStreamBasicDescription * inSourceFormat,
                        const AudioStreamBasicDescription * inDestinationFormat,
                        AudioConverterRef __nullable * __nonnull outAudioConverter) 

需要把我们转换的LPCM格式回调输入AudioConverterFillComplexBuffer

extern OSStatus
AudioConverterFillComplexBuffer(    AudioConverterRef                   inAudioConverter,
                                    AudioConverterComplexInputDataProc  inInputDataProc,
                                    void * __nullable                   inInputDataProcUserData,
                                    UInt32 *                            ioOutputDataPacketSize,
                                    AudioBufferList *                   outOutputData,
                                    AudioStreamPacketDescription * __nullable outPacketDescription)

AudioConverterComplexInputDataProc回调函数就是读取原有数据的帧数据 放置于ioData中

static OSStatus BXPlayerConverterFiller(AudioConverterRef inAudioConverter, UInt32 * ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription** outDataPacketDescription, void * inUserData)
{
   
    BXAudioUnitEQPlayer *self = (__bridge BXAudioUnitEQPlayer *)(inUserData);
    
    if (self->_readPacketIndex >= self->_packetArray.count) {
        *ioNumberDataPackets = 0;
        return 'bxnd';
    }
    NSData *packet = self->_packetArray[self->_readPacketIndex];
    ioData->mNumberBuffers = 1;
    ioData->mBuffers[0].mData = (void *)packet.bytes;
    ioData->mBuffers[0].mDataByteSize = (UInt32)packet.length;
    
    static AudioStreamPacketDescription aspdesc;
    aspdesc.mDataByteSize = (UInt32)packet.length;
    aspdesc.mStartOffset = 0;
    aspdesc.mVariableFramesInPacket = 1;
    *outDataPacketDescription = &aspdesc;
    self->_readPacketIndex++;
     *ioNumberDataPackets = 1;
    return noErr;
}

四、附录

AudioUnit掌握需要同学们切合实际的敲代码运用AudioUnit 使用的简单示例

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容