音频编解码基础篇之AudioUnit-1-裸创建播放

一.AudioUnit

1.简要

在iOS平台上,所有的音频框架底层都是基于AudioUnit实现的。较高层次的音频框架包括:Media Player、AV Foundation、OpenAL和Audio Toolbox,这些框架都封装了AudioUnit,然后提供了更高层次的API(功能更少,职责更单一的接口)。


i/o原型.jpg

在 cocoa 中的位置.jpg
2.应用场景

1.想使用低延迟的音频I/O(input或者output),比如说在VoIP的应用场景下。
2.多路声音的合成并且回放,比如游戏或者音乐合成器的应用。
3.使用AudioUnit里面提供的特有功能,比如:回声消除、Mix两轨音频,以及均衡器、压缩器、混响器等效果器。
4.需要图状结构来处理音频,可以将音频处理模块组装到灵活的图状结构中,苹果公司为音频开发者提供了这种API。

3.创建简要流程

1)在运行时,获取对动态链接库的引用,该库定义要使用的音频单元。
2)实例化音频单元。
3)按照其类型的需要配置音频单元,以适应您的应用程序的意图。
4)初始化音频单元以准备处理音频。
5)开始音频流。
6)控制音频单元。
7)完成后,销毁音频单元

4.创建方式

1.裸创建方式
2.AUGraph方式创建
现在进入AudioUnit实践开篇主题,使用第一种方式创建AudioUnit

#import "AudioUnitManger.h"
#import <AudioToolbox/AudioToolbox.h>
@interface AudioUnitManger()<NSURLSessionDelegate>
{
    NSInteger _readedPacketIndex;
    UInt32 _renderBufferSize;
    
    AudioUnit _outAudioUinit;
    AudioStreamBasicDescription _streamDescription;
    AudioFileStreamID _audioFileStreamID;
    AudioBufferList *_renderBufferList;
    AudioConverterRef _converter;
}
@property(nonatomic,strong) NSMutableArray<NSData*> *paketsArray;
@end
@implementation AudioUnitManger

//调用AudioConverterFillComplexBuffer传入数据,并在callBack函数调用填充buffer的方法。
OSStatus  DJAURenderCallback(void *inRefCon,AudioUnitRenderActionFlags *    ioActionFlags,const AudioTimeStamp *inTimeStamp,UInt32    inBusNumber,UInt32 inNumberFrames, AudioBufferList * __nullable ioData){
    AudioUnitManger *self = (__bridge AudioUnitManger *)(inRefCon);
    @synchronized (self) {
        if (self->_readedPacketIndex < self.paketsArray.count) {
            @autoreleasepool {
                UInt32 packetSize = inNumberFrames;
                OSStatus status = AudioConverterFillComplexBuffer(self->_converter, DJAudioConverterComplexInputDataProc, (__bridge void *)self, &packetSize, self->_renderBufferList, NULL);
                if (status != noErr && status != 'DJnd') {
                    [self stop];
                    return -1;
                }
                else if (!packetSize) {
                    ioData->mNumberBuffers = 0;
                }
                else {
                    ioData->mNumberBuffers = 1;
                    ioData->mBuffers[0].mNumberChannels = 2;
                    ioData->mBuffers[0].mDataByteSize = self->_renderBufferList->mBuffers[0].mDataByteSize;
                    ioData->mBuffers[0].mData =self->_renderBufferList->mBuffers[0].mData;
                    self->_renderBufferList->mBuffers[0].mDataByteSize = self->_renderBufferSize;
                }
            }
        }
        else {
            ioData->mNumberBuffers = 0;
            return -1;
        }
    }
    return noErr;
    
}
//歌曲信息解析回调将传递给回调的常量。
//每当在数据中分析属性的值时,都将回调
void DJAudioFileStream_PropertyListenerProc(void *    inClientData,AudioFileStreamID                inAudioFileStream,AudioFileStreamPropertyID    inPropertyID,AudioFileStreamPropertyFlags *    ioFlags)
{
    if (inPropertyID == kAudioFileStreamProperty_DataFormat) {
        
        AudioUnitManger *self = (__bridge AudioUnitManger *)(inClientData);
        UInt32 dataSize = 0;
        Boolean writable = false;
        OSStatus status = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &dataSize, &writable);
        assert(status == noErr);
        
        status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &dataSize, &self->_streamDescription);
        assert(status == noErr);
        AudioStreamBasicDescription destFormat = audioStreamBasicDescription();
        status = AudioConverterNew(&self->_streamDescription, &destFormat, &self->_converter);
        assert(status == noErr);
    }
    
}
//解析分离帧回调
//每当在数据中分析数据包时,都会向客户机传递指向数据包的指针。开始回调。
void DJAudioFileStreamPacketsProc(void *inClientData,UInt32 inNumberBytes,UInt32                            inNumberPackets,const void *inInputData,AudioStreamPacketDescription *inPacketDescriptions)
{
    AudioUnitManger *self = (__bridge AudioUnitManger *)(inClientData);
    if (inPacketDescriptions) {
        for (int i = 0; i < inNumberPackets; i++) {
            SInt64 packetOffset = inPacketDescriptions[i].mStartOffset;
            UInt32 packetSize = inPacketDescriptions[i].mDataByteSize;
            assert(packetSize > 0);
            NSData *packet = [NSData dataWithBytes:inInputData + packetOffset length:packetSize];
            [self.paketsArray addObject:packet];
        }
    }
    if (self->_readedPacketIndex == 0 && self.paketsArray.count > [self packetsPerSecond] * 3) {
        [self play];
        
    }
}

- (double)packetsPerSecond
{
    if (!(_streamDescription.mFramesPerPacket > 0)) {
        return 0;
    }
    return _streamDescription.mSampleRate / _streamDescription.mFramesPerPacket;
}
/*
 AudioFileStreamOpen (
 void * __nullable                        inClientData,
 AudioFileStream_PropertyListenerProc    inPropertyListenerProc,
 AudioFileStream_PacketsProc                inPacketsProc,
 AudioFileTypeID                            inFileTypeHint,
 AudioFileStreamID __nullable * __nonnull outAudioFileStream)
 向解析器提供数据,当在数据中发现有内容(如属性和音频包)回调
 @参数包数据 inClientData
 @参数infiletypehint
 对于无法根据数据轻松或唯一确定其类型的文件(ADTS、AC3),
 此提示可用于指示文件类型。
 否则,如果您不知道文件类型,则可以传递零。
 @参数outaudiofilestream
 用于其他audiofilestream api调用的新文件流ID。
 */
//inClientData 上下文对象
//AudioFileStream_PropertyListenerProc 歌曲信息解析的回调,每次解析出一个歌曲信息,都会执行一次回调。
//AudioFileStream_PacketsProc 分离帧的回调,每解析出一部分帧就会进行一次回调
//AudioFileTypeID 是文件类型的提示,创建指定文件格式的音频流解析器。

-(instancetype)initWithURL:(NSURL*)url
{
    if (self = [super init]) {
        _paketsArray = [NSMutableArray arrayWithCapacity:0];
        [self setupOutAudioUnit];
        AudioFileStreamOpen((__bridge void * _Nullable)(self), DJAudioFileStream_PropertyListenerProc, DJAudioFileStreamPacketsProc, 0, &_audioFileStreamID);
        NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
        NSURLSessionDataTask *task = [urlSession dataTaskWithURL:url];
        [task resume];
    }
    return self;
}
-(void)setupOutAudioUnit
{
    //    构造RemoteIO类型的AudioUnit描述的结构体
    AudioComponentDescription ioUnitDescription;
    memset(&ioUnitDescription, 0, sizeof(AudioComponentDescription));
    ioUnitDescription.componentType = kAudioUnitType_Output;
    ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
    ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    ioUnitDescription.componentFlags = 0;
    ioUnitDescription.componentFlagsMask = 0;
    
    //    首先根据AudioUnit的描述,找出实际的AudioUnit类型:
    AudioComponent outComponent = AudioComponentFindNext(NULL, &ioUnitDescription);
    //    根据AudioUnit类型创建出这个AudioUnit实例:
    OSStatus status = AudioComponentInstanceNew(outComponent, &_outAudioUinit);
    assert(status == noErr);
    //  Audio Stream Format的描述,构造BasicDescription结构体
    AudioStreamBasicDescription pcmStreamDesc = audioStreamBasicDescription();
    
    OSStatus statusSetProperty = noErr;
    //   将这个结构体设置给对应的AudioUnit,将这Unit的Element0的Out-putScope和Speaker进行连接使用扬声器
    statusSetProperty = AudioUnitSetProperty(_outAudioUinit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &pcmStreamDesc, sizeof(pcmStreamDesc));
    //    AudioUnitSetProperty(AudioUnit                inUnit,
    //                         AudioUnitPropertyID        inID,
    //                         AudioUnitScope            inScope,
    //                         AudioUnitElement        inElement,
    //                         const void * __nullable    inData,
    //                         UInt32                    inDataSize)
    //    构造一个AURenderCallback的结构体,并指定一个回调函数,然后设置给RemoteIO Unit的输入端,当RemoteIO Unit需要数据输入的时候就会回调该回调函数
    AURenderCallbackStruct callBackStruct;
    callBackStruct.inputProc = DJAURenderCallback;
    callBackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
    
    AudioUnitSetProperty(_outAudioUinit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, 0, &callBackStruct, sizeof(AURenderCallbackStruct));
    
    UInt32 bufferSize = 4096 * 4;
    _renderBufferSize = bufferSize;
    _renderBufferList = calloc(4, sizeof(UInt32)+sizeof(bufferSize));
    _renderBufferList->mNumberBuffers = 1;
    _renderBufferList->mBuffers[0].mData = calloc(1, bufferSize);
    _renderBufferList->mBuffers[0].mDataByteSize = bufferSize;
    _renderBufferList->mBuffers[0].mNumberChannels = 2;
    
}

static AudioStreamBasicDescription audioStreamBasicDescription()
{
    AudioStreamBasicDescription description;
    description.mSampleRate = 44100.0;
    description.mFormatID = kAudioFormatLinearPCM;
    description.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
    description.mFramesPerPacket = 1;
    description.mBytesPerPacket = 4;
    description.mBytesPerFrame = 4;
    description.mChannelsPerFrame = 2;
    description.mBitsPerChannel = 16;
    description.mReserved = 0;
    //    msamplerate:流中每秒数据的采样帧数。
    //    mformatid:在流中指定常规音频数据格式的标识符。(KaudioFormilanePCM等)
    //    mFormatflags:格式化特定标志以指定格式的详细信息。(klinearpcmFormatflagIsignedInteger、klinearpcmFormatflagIsFloat、klinearpcmFormatflagIsBigEndian、klinearpcmFormatflagIsPackaged、klinearpcmFormatflagIsOnInterleaved等)
    //    mbytesperpacket:数据包中的字节数。
    //    mframesPerpacket:每个数据包中的样本帧数。
    //    mbytespeframe:单个数据采样帧中的字节数。
    //    mchannelsPerFrame:每帧数据中的通道数
    //    mbitsPerchannel:数据帧中每个通道的采样数据位数。
    //    mReserved将结构垫出以强制8字节对齐
    return description;
}

OSStatus DJAudioConverterComplexInputDataProc(AudioConverterRef inAudioConverter,UInt32 * ioNumberDataPackets,AudioBufferList *  ioData,AudioStreamPacketDescription * __nullable * __nullable outDataPacketDescription,void * __nullable inUserData)
{
    AudioUnitManger *self = (__bridge AudioUnitManger *)(inUserData);
    if (self->_readedPacketIndex >= self.paketsArray.count) {
        NSLog(@"Have No Data");
        return -1;
    }
    
    //    填充PCM到缓冲区
    NSData *packet = self.paketsArray[self->_readedPacketIndex];
    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->_readedPacketIndex++;
    return 0;
    
}

#pragma mark -NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    OSStatus status = AudioFileStreamParseBytes(_audioFileStreamID, (UInt32)data.length, data.bytes, 0);
    if (status == noErr) {
        
    }
}

- (BOOL)play
{
    OSStatus status = AudioOutputUnitStart(_outAudioUinit);
    if (status == noErr) {
        return YES;
    }
    return 0;
}

- (BOOL)stop
{
    OSStatus status = AudioOutputUnitStop(_outAudioUinit);
    if (status == noErr) {
        return YES;
    }
    return 0;
}
@end
//播放
AudioUnitManger *player = [[AudioUnitManger alloc] initWithURL:[NSURL URLWithString:@"http://www.ytmp3.cn/down/58627.mp3"]];
[player play];

Github
音频编解码基础篇之AudioUnit-2-AUGraph方式创建并集成麦克风
音频编解码基础篇之AudioUnit-3-AUGraph方式创建并集成麦克风&混音等特效

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