iOS AudioUnit学习

AudioUnit是iOS底层音频框架,相比于AudioQueue和AVAudioRecorder,能够对音频数据进行更多的控制,可以用来进行混音、均衡、格式转换、实时IO录制、回放、离线渲染等音频处理。

1、AudioUnit录音:

先看下苹果官方的原理图(此处以I/O单元为例):


image.png

1、一个AudioUnit包含2个element。
2、每个element包含输入输入部分(scope)。
3、硬件到element的部分(即图中淡蓝色部分)我们无法介入,我们能控的就element与我们APP关联的部分(即图中淡黄色部分)。
4、实现录音就是主要关注element1到APP的过程,播放则是关注APP到element0的过程。

代码实现:
代码只实现录音功能,所以只使用了element1:

设置音频格式:

-  (void)initRecordFormat {
    _recordFormat.mSampleRate =  32000;  //采样率
    _recordFormat.mChannelsPerFrame = 1; //声道数量
    //编码格式
    _recordFormat.mFormatID = kAudioFormatLinearPCM;
    _recordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
    //每采样点占用位数
    _recordFormat.mBitsPerChannel = 16;
    //每帧的字节数
    _recordFormat.mBytesPerFrame = (_recordFormat.mBitsPerChannel / 8) * _recordFormat.mChannelsPerFrame;
    //每包的字节数
    _recordFormat.mBytesPerPacket = _recordFormat.mBytesPerFrame;
    //每帧的字节数
    _recordFormat.mFramesPerPacket = 1;
}

接着实例化AudioUnit,配置相关属性和方法

- (void)initConfig {
    //配置描述
    AudioComponentDescription acd;
    acd.componentType = kAudioUnitType_Output;
    acd.componentManufacturer = kAudioUnitManufacturer_Apple;
    //remoteIO对应的就是(I/O)Unit
    acd.componentSubType = kAudioUnitSubType_RemoteIO;
    acd.componentFlags = 0;
    acd.componentFlagsMask = 0;
    
    //AudioComponent类似组件工厂,用于实例化audioUnit
    AudioComponent comp = AudioComponentFindNext(nil, &acd);
    OSStatus status =  AudioComponentInstanceNew(comp, &_audioUnit);
    if(status!= kAudioSessionNoError) {
        NSLog(@"InstanceError");
        return;
    }
    
    //开启麦克风到Element1的inputScope部分,参数1代表element1。
    UInt32 flag=1;
    OSStatus statusPerpotyIO =  AudioUnitSetProperty(_audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flag, sizeof(flag));  
    if(statusPerpotyIO!= kAudioSessionNoError) {
        NSLog(@"SetPropertyIOError");
        return;
    }
  
  
    //设置Element1到APP的outScope部分的流格式(即我们需要得到的音频数据格式)
    OSStatus statusPerpotyFR =  AudioUnitSetProperty(_audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &_recordFormat, sizeof(_recordFormat));
    if(statusPerpotyFR!= kAudioSessionNoError) {
        NSLog(@"SetPropertyFRError");
        return;
    }
    
    //设置录制过程的回调函数
    AURenderCallbackStruct callBackSt;
    callBackSt.inputProc = inputCallBack;
    //将对象指针传入回调函数内部,方便内部使用
    callBackSt.inputProcRefCon = (__bridge void * _Nullable)(self);
    OSStatus statusPerpotyCall =  AudioUnitSetProperty(_audioUnit,
                                                        kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &callBackSt, sizeof(callBackSt));
    if(statusPerpotyCall!= kAudioSessionNoError) {
        NSLog(@"SetPropertyCallError");
        return;
    }
}

设置AudioSession模式,开启

- (void)setAudioSessionEnable:(BOOL)YesOrNo {
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    [[AVAudioSession sharedInstance] setActive:YesOrNo error:nil];
}

启动audioUnit,开始录音

- (void)startRecord {
    OSStatus statusInit = AudioUnitInitialize(_audioUnit);
    if(statusInit!= kAudioSessionNoError) {
        NSLog(@"statusInitError");
        return;
    }
    
    AudioOutputUnitStart(_audioUnit);
    self.isRecording = YES;
}

回调函数实现,获取音频数据:

OSStatus   inputCallBack    (void *                            inRefCon,
                              AudioUnitRenderActionFlags *      ioActionFlags,
                              const AudioTimeStamp *            inTimeStamp,
                              UInt32                            inBusNumber,
                              UInt32                            inNumberFrames,
                              AudioBufferList * __nullable      ioData)
{
    
    YTAudioUnitManager *audioManager = [YTAudioUnitManager sharedManager];
    //创建bufferlist
    AudioBufferList bufferList;
    bufferList.mNumberBuffers = 1;
    bufferList.mBuffers[0].mDataByteSize = sizeof(SInt16)*inNumberFrames;
    bufferList.mBuffers[0].mNumberChannels = 1;
    bufferList.mBuffers[0].mData = (SInt16*) malloc(sizeof(SInt16)*inNumberFrames);
    
    //将unit的数据渲染到bufferList
    OSStatus status =  AudioUnitRender(audioManager.audioUnit,
                    ioActionFlags,
                    inTimeStamp,
                    1,
                    inNumberFrames,
                    &bufferList);
    
    //这里是将数据写入文件
   // ExtAudioFileWrite(audioManager.fileInfo->extAudioFileRef, inNumberFrames, &bufferList);
    
    return status;
}

停止录音:

- (void)stopRecord {
    [self setAudioSessionEnable:NO];
    AudioOutputUnitStop(_audioUnit);
    AudioUnitUninitialize(_audioUnit);
    ExtAudioFileDispose(_fileInfo->extAudioFileRef);
    self.isRecording = NO;
}

2、AudioUnit混音:

如果只是使用AudioUnit实现播放和录音功能,未免太大材小用,我们来看看混音是如何实现的。
混音是把多种来源的声音,整合至一个立体音轨(Stereo)或单音音轨(Mono)中,此处是读取两段音频,由左右声道同时进行播放。

官方也很贴心的给出了demo:Apple官方Demo地址

原理:
首先我们看下官方的使用原理图:

image.png

1、整体是AudioGraph,用来管理多个AudioUnit,此处涉及到是I/O单元和Mix单元。
2、两个音频数据源,提供数据给Mix Unit,Mix Unit具有多个input通道,但是只有一个输出通道。
3、Mix Unit将处理完的数据传输给I/O单元,I/O单元负责播放。

具体实现:

变量定义:

//定义结构体用来保存音频数据的信息
typedef struct {
    AudioStreamBasicDescription asbd;
    Float32 *data;
    UInt32 numFrames;
    UInt32 startFrameNum;
} SoundBuffer;

@interface YTAudioMixManager() {
    SoundBuffer mSoundBuffer[2];  //两个音频文件
}
@property (nonatomic,assign)AUGraph auGraph;
//针对音频文件的格式
@property (nonatomic,strong)AVAudioFormat *fileFormat;
//针对unit的格式
@property (nonatomic,strong)AVAudioFormat *unitFormat;
@end

初始化Format:

-  (void)initRecordFormat {
     //Audiounit的描述 ,
     //声道为2,
    //interleaved为NO,使左右声道的数据分别存储在AudioBufferList的两个AudioBuffer中。
     _unitFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
                                                    sampleRate:44100
                                                      channels:2
                                                   interleaved:NO];
    //文件音数据的描述 ,
    //声道为1,
    //interleaved为YES
    _fileFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
                                                   sampleRate:44100
                                                     channels:1
                                                  interleaved:YES];
}

读取两个音频文件数据:

//读取音频文件数据
- (void)loadDataFromURLS:(NSArray *)urlNames {
    for (int i =0;i<urlNames.count;i++) {
        
        NSString *urlName = urlNames[i];
        CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)urlName, NULL);
        ExtAudioFileRef audioFileRef;
        ExtAudioFileOpenURL(url, &audioFileRef);
        
        
        OSStatus status;
        UInt32 propSize = sizeof(AudioStreamBasicDescription);
        //设置我们想要要获取的音频文件格式,ExtAudioFile是自带转码功能的。
        status = ExtAudioFileSetProperty(audioFileRef, kExtAudioFileProperty_ClientDataFormat, propSize, _fileFormat.streamDescription);
        if(status!=kAudioSessionNoError) {
            NSLog(@"FileGetProperty Error");
            return;
        }
        
        AudioStreamBasicDescription fileFormat;
        UInt32 formSize = sizeof(fileFormat);
     //读取文件格式属性  
        status =  ExtAudioFileGetProperty(audioFileRef, kAudioFileStreamProperty_FileFormat, &formSize, &fileFormat);
        if(status!=kAudioSessionNoError) {
            NSLog(@"FileGetProperty Error");
            return;
        }
        //读取文件帧数 
        UInt64 numFrames;
        UInt32 numFramesSize = sizeof(numFrames);
        status = ExtAudioFileGetProperty(audioFileRef, kExtAudioFileProperty_FileLengthFrames, &numFramesSize, &numFrames);
        if(status!=kAudioSessionNoError) {
            NSLog(@"FileGetProperty Error");
            return;
        }
        
        mSoundBuffer[i].numFrames = (UInt32)numFrames;
        mSoundBuffer[i].asbd = fileFormat;
        
        UInt64 samples = numFrames * mSoundBuffer[i].asbd.mChannelsPerFrame;
        mSoundBuffer[i].data = (Float32 *)calloc(samples, sizeof(Float32));
        mSoundBuffer[i].sampleNum = 0;
        
        //将文件数据读取传入BufferList
        AudioBufferList bufList;
        bufList.mNumberBuffers = 1;
        bufList.mBuffers[0].mNumberChannels = 1;
        bufList.mBuffers[0].mData = (Float32*)malloc(sizeof(Float32)*samples);
        bufList.mBuffers[0].mDataByteSize = (Float32)samples * sizeof(Float32);
        UInt32 numPackets = (UInt32)numFrames;
        status = ExtAudioFileRead(audioFileRef, &numPackets, &bufList);
        
        //将bufferList数据复制给mSoundBuffer
        memcpy(mSoundBuffer[i].data, bufList.mBuffers[0].mData , bufList.mBuffers[0].mDataByteSize);
        
        if(status!=kAudioSessionNoError) {
//            printf("ExtAudioFileRead Error");
            free(mSoundBuffer[i].data);
            mSoundBuffer[i].data = 0;
        }
        ExtAudioFileDispose(audioFileRef);
    }
}

设置AudioUnit相关对象

- (void)initAudioUnit {
    
    //IO单元描述
    AudioComponentDescription IOacd;
    IOacd.componentType = kAudioUnitType_Output;
    IOacd.componentManufacturer = kAudioUnitManufacturer_Apple;
    //remoteIO对应的就是(I/O)Unit
    IOacd.componentSubType = kAudioUnitSubType_RemoteIO;
    IOacd.componentFlags = 0;
    IOacd.componentFlagsMask = 0;
    
    //mix单元描述
    AudioComponentDescription mixacd;
    mixacd.componentType = kAudioUnitType_Mixer;
    mixacd.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixacd.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixacd.componentFlags = 0;
    mixacd.componentFlagsMask = 0;
    
    OSStatus status;
    status=  NewAUGraph (&_auGraph);
    if(status!=kAudioSessionNoError) {
        NSLog(@"NewAUGraph Error");
         return;
    }
    
    
    AUNode ioNode;
    AUNode mixNode;
    
    AudioUnit ioUnit;
    AudioUnit mixUnit;
    
    //添加node
    AUGraphAddNode (_auGraph,&IOacd,&ioNode);
    AUGraphAddNode (_auGraph,&mixacd, &mixNode);
    
    //建立两个node的输入和输出连接
    status = AUGraphConnectNodeInput(_auGraph, mixNode, 0, ioNode, 0);
    if (status!=kAudioSessionNoError) {
        printf("AUGraphConnect Error");
        return;
    }

    AUGraphOpen (_auGraph);
    if(status!=kAudioSessionNoError) {
        NSLog(@"AUGraphOpen Error");
         return;
    }
    
    //获取node对应的Aunit
    status = AUGraphNodeInfo(_auGraph, mixNode, NULL, &mixUnit);
    if(status!=kAudioSessionNoError) {
        NSLog(@"AUGraphNodeMixInfo Error");
        return;
    }
    status = AUGraphNodeInfo(_auGraph, ioNode, NULL, &ioUnit);
    if(status!=kAudioSessionNoError) {
        NSLog(@"AUGraphOpen Error");
        return;
    }
    //设置输入数量
    int elementCount = 2;
    status = AudioUnitSetProperty(mixUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &elementCount, sizeof(elementCount));
    if(status!=kAudioSessionNoError) {
        NSLog(@"AudioUnitSetProperty Error");
        return;
    }
    

    
    for (int i = 0; i < elementCount;i++) {
        // setup render callback struct
        AURenderCallbackStruct rcbs;
        rcbs.inputProc = &mixInputCallBack;
        rcbs.inputProcRefCon = mSoundBuffer;
        
        //给mix的两个element设置回调
        status = AUGraphSetNodeInputCallback(_auGraph, mixNode, i, &rcbs);
       if (status) { printf("AUGraphSetNodeInputCallback result %ld %08lX %4.4s\n", (long)status, (long)status, (char*)&status); return; }
        //设置输入格式
        status = AudioUnitSetProperty(mixUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i,_unitFormat.streamDescription , sizeof(AudioStreamBasicDescription));
        
        if(status!=kAudioSessionNoError) {
            NSLog(@"AudioUnitSetProperty Error");
            return;
        }
    }
    
    
    status = AudioUnitSetProperty(mixUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, _unitFormat.streamDescription, sizeof(AudioStreamBasicDescription));
    if (status) {
        NSLog(@"AudioUnitSetProperty Error");
        return;
    }
    
    status = AudioUnitSetProperty(ioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, _unitFormat.streamDescription, sizeof(AudioStreamBasicDescription));
    if (status) {
        NSLog(@"AudioUnitSetProperty Error");
        return;
    }
    
    
    status = AUGraphInitialize(_auGraph);
    if (status) {
        NSLog(@"AudioUnitSetProperty Error");
        return;
    }
}

实现回调的方法,填充数据供输出:

OSStatus mixInputCallBack (void *                            inRefCon,
                           AudioUnitRenderActionFlags *      ioActionFlags,
                           const AudioTimeStamp *            inTimeStamp,
                           UInt32                            inBusNumber,
                           UInt32                            inNumberFrames,
                           AudioBufferList * __nullable      ioData) {
    
    SoundBuffer *sndbuf = (SoundBuffer *)inRefCon;
    
    UInt32 startFrame = sndbuf[inBusNumber].startFrameNum;      // 从哪一帧开始
    UInt32 numFrames = sndbuf[inBusNumber].numFrames;  // 总的帧数
    Float32 *bufferData = sndbuf[inBusNumber].data; // audio data buffer
    
    Float32 *outA = (Float32 *)ioData->mBuffers[0].mData; //  第一声道数据
    Float32 *outB = (Float32 *)ioData->mBuffers[1].mData; //  第二声道数据
    

    
    for (UInt32 i = 0; i < inNumberFrames; ++i) {
        if (inBusNumber == 0) {
            outA[i] = bufferData[startFrame++];   //填充第一声道数据(用的是第一个SoundBuffer)
        } else {
            outB[i] = bufferData[startFrame++];   //填充第二声道数据(用的是第二个SoundBuffer)
        }
    
        if (startFrame > numFrames) {
            // 结束了,再从0开始循环
            printf("looping data for bus %d after %ld source frames rendered\n", (unsigned int)inBusNumber, (long)startFrame-1);
            startFrame = 0;
        }
    }
    sndbuf[inBusNumber].startFrameNum = startFrame; // 记录帧数
    return noErr;
}

总结:AudioUnit更像是个看图编程的过程ㄟ( ▔, ▔ )ㄏ。

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