Audio Queue详解

作者: 流星泪x | 来源:发表于2019-10-08 15:29 被阅读0次

    一、前言

    Audio Queue Services提供了一种简单的、低开销的方式来录制、播放iOS和Mac OS X中的音频。用于为iOS或Max OS X应用添加基本的录制和播放功能。

    Audio Queue Services 能允许你录制和播放下面这些格式的音频:

    • Linear PCM.
    • Apple平台上支持的任何压缩格式,如aac、mp3、m4a等
    • 用户已安装编码器的任何其他格式

    Audio Queue Services是一个上层服务。它让你的应用使用硬件(如麦克风、扬声器)录制和播放,而无需了解硬件接口。它还允许你使用复杂的编解码器,而无需了解编解码器的工作方式。

    二、什么是Audio Queue

    Audio Queue是一个对象,用于录制和播放音频。它用AudioQueueRef数据类型表示,在AudioQueue.h头文件中声明

    Audio Queue做以下任务:

    • Connecting to audio hardware(连接音频硬件)
    • Managing memory(管理内存)
    • Employing codecs, as needed, for compressed audio formats(根据需要,为压缩音频格式使用编解码器)
    • Mediating recording or playback(调解录音和播放)

    三、Audio Queue架构

    所有的Audio Queue都有着相同的结构,由以下部分组成:

    • 一组audio queue buffers,每个buffer临时存储着audio data
    • 一个buffer queue,有序的管理audio queue buffers
    • 一个audio queue callback,这个需要开发者来编写

    架构取决于音频队列是用于录制还是回放。 不同之处在于音频队列如何连接其输入和输出,以及回调函数的作用。

    3.1 Audio Queues for recording

    使用AudioQueueNewInput函数创建一个recording audio queue,其结构如下图:

    recording.png

    输入方通常连接的是音频硬件,比如麦克风。在iOS中,音频通常来自用户内置的麦克风或耳机麦克风连接的设备 。在Mac OS X的默认情况下,音频来自系统首选项中用户设置的系统默认音频输入设备。

    audio queue的输出端,是开发者编写的回调函数。当录制到磁盘时,回调函数会将buffers中的新数据写入到文件,buffers中的数据是从audio queue中接收到的。但是,recording audio queue还可以有其它用途。比如,你的回调函数直接向你的应用提供audio data而不是将其写入磁盘。

    每个audio queue,无论是recording或playback,都有一个或多个audio queue buffers。这些buffers按特定的顺序排列。在上图中,audio queue buffers按照它们被填充的顺序进行编号,这与它们切换到回调的顺序相同。 后面会详细说明如何使用。

    3.2 Audio Queues for Playback

    使用AudioQueueNewOutput函数创建一个playback audio queue,其结构如下图:

    playback.png
    在playback audio queue中,回调函数是在输入端;回调函数负责从磁盘(或其他一些源)获取音频数据并将其交给音频队列。 当没有更多数据可播放时,回调函数应该告知audio queue停止播放

    playback audio queue的输出端通常连接的audio硬件设备,如扬声器

    3.3 Audio Queue Buffers

    Audio Queue Buffers是一个类型为AudioQueueBuffer结构体

    typedef struct AudioQueueBuffer {
        const UInt32   mAudioDataBytesCapacity;
        void *const    mAudioData;
        UInt32         mAudioDataByteSize;
        void           *mUserData;
    } AudioQueueBuffer;
    typedef AudioQueueBuffer *AudioQueueBufferRef;
    

    mAudioData:指向了存储音频数据的内存块
    mAudioDataByteSize:audio data的字节数,在录制的时候audio queue会设置此值;在播放的时候,需要开发者来设置

    Audio Queue可以使用任意数量的buffers。 你的应用程序指定了多少。 通常是三个。 一个用来写入磁盘,而另一个用来填充新的音频数据。 如果需要,可以使用第三个缓冲区来补偿磁盘I / O延迟等问题

    Audio Queue为其buffers执行内存管理。

    • 使用AudioQueueAllocateBuffer函数创建一个buffer
    • 使用AudioQueueDispose函数释放一个Audio Queue,Audio Queue会释放掉它的buffers

    四、The Buffer Queue and Enqueuing

    下面将分析Audio Queue对象如何在录制和播放期间管理buffer queue,以及enqueuing

    4.1 录制过程
    录制过程.png

    步骤1:开始录制,audio queue填充数据到buffer1
    步骤2:buffer1填充满后,audio queue调用回调函数处理buffer1(步骤3);与此同时audio queue填充数据到buffer2
    步骤4:将用过的buffer1重新入队,再将填充好的buffer2给回调使用(步骤6),与此同时audio queue填充数据到其它的buffer(步骤5);依此循环,直到停止录制

    4.2 播放过程

    播放时,一个audio queue buffer 被发送到输出设备,如扬声器。在queue buffer中的剩余buffers排在当前buffer后面,等待依此播放。

    Audio queue会按照播放顺序将播放的音频数据buffer交给回调函数,回调函数将新的音频数据读入buffer,然后将其入队

    播放过程.png
    步骤1:应用程序启动playback audio queue。应用程序为每个audio queue buffers调用一次回调,填充它们并将它们添加到buffer queue。当你的应用程序调用AudioQueueStart函数时确保能够启动。
    步骤3:audio queue 发送buffer1到输出设备
    一旦播放了第一个buffer,playback audio queue进入一个循环状态。audio queue开始播放下一个buffer(buffer2 步骤4)并调用回调处理刚刚播放完的buffer(步骤5),填充buffer,并将其入队(步骤6)
    4.3 控制播放过程

    Audio queue buffers始终按照入队顺序进行播放,但是audio queue可以使用AudioQueueEnqueueBufferWithParameters函数对播放过程进行一些控制

    • Set the precise playback time for a buffer. This lets you support synchronization.
    • Trim frames at the start or end of an audio queue buffer. This lets you remove leading or trailing silence.
    • Set the playback gain at the granularity of a buffer

    五、The Audio Queue Callback Function

    通常,使用Audio Queue Services大部分工作是编写回调函数。在录制和播放时,audio queue会重复的调用callback。调用之间的时间取决于audio queue buffers的容量,通常为半秒到几秒

    5.1 The Recording Audio Queue Callback Function
    typedef void (*AudioQueueInputCallback)(
            void * __nullable               inUserData,
            AudioQueueRef                   inAQ,
            AudioQueueBufferRef             inBuffer,
            const AudioTimeStamp *          inStartTime,
            UInt32                          inNumberPacketDescriptions,
            const AudioStreamPacketDescription * __nullable inPacketDescs
    );
    

    当recording audio queue调用callback时,会提供下一组音频数据所需的内容

    • inUserData:通常是传入一个包含audio queue及其buffer的状态信息的对象/结构体
    • inAQ:调用callback的audio queue
    • inBuffer:audio queue buffer,有audio queue刷新填充,其包含了你的回调需要写入磁盘的新数据。该数据已根据在inUserData中指定的格式进行格式化。
    • inStartTime:buffer第一个样本的采样时间,对应基本录制,你的callback不使用该参数
    • inNumberPacketDescriptions:是inPacketDescs参数中的数据包描述个数。如果要录制为VBR(可变比特率)格式,音频队列会为您的回调提供此参数的值,然后将其传递给AudioFileWritePackets函数。 CBR(恒定比特率)格式不使用数据包描述。 对于CBR记录,音频队列将此设置和inPacketDescs参数设置为NULL。
    • inPacketDescs:buffer中的样本对应的数据包描述集。 同样,如果音频数据是VBR格式,音频队列将提供此参数的值,并且您的回调将其传递给AudioFileWritePackets函数
    5.2 The Playback Audio Queue Callback Function
    typedef void (*AudioQueueOutputCallback)(
            void * __nullable       inUserData,
            AudioQueueRef           inAQ,
            AudioQueueBufferRef     inBuffer
    );
    

    在调用callback是,playback audio queue提供callback读取下一组音频数据所需的内容

    • inUserData:通常传入一个包含audio queue及其buffers的状态信息对象/结构体
    • inAQ:调用callback的audio queue
    • inBuffer:由audio queue提供,你的callback将填充从正在播放的文件中读取下一组数据

    如果你的应用程序播放VBR数据,callback需要获取audio data的packet information。使用AudioFileReadPacketData函数,然后将packet information放入inUserData中,以使其可用于playback audio queue

    六、使用Codecs and Audio Data Formats

    Audio Queue Services根据需要使用编解码器(音频数据编码/解码组件)来在音频格式之间进行转换。 你的录制或播放应用程序可以使用已安装编解码器的任何音频格式。 你无需编写自定义代码来处理各种音频格式。 具体来说,你的回调不需要了解数据格式。

    每个Audio queue具有audio data format,使用结构体AudioStreamBasicDescription来表示。当你指定foramt结构体中的mFromatID字段的格式时,audio queue会使用合适的编解码器。然后,你可以指定采样率和声道数,这就是它的全部内容。

    recording_use_codec.png

    录制使用Codec:
    步骤1:应用程序开始recording,并告知audio queue使用何种format来编码
    步骤2:根据fromat选择合适的codec得到压缩数据提交给callback
    步骤3:callback调用写入磁盘

    playback_use_codec.png

    播放使用Codec:
    步骤1:应用程序告知audio queue接收何种format的数据,并启动playing
    步骤2:audio queue调用callback,从audio file中读取数据,callback将原始数据传递给audio queue。
    步骤3:audio queue使用合适的codec将数据转为未压缩的数据发送到目的地

    七、Audio Queue Control and State

    Audio queue在creation和disposal之间具有生命周期。你的应用程序可以使用以下函数来管理它的生命周期。

    • Start(AudioQueueStart)在初始化recording或playback时调用
    • Prime(AudioQueuePrime)对于playback,需要在AudioQueueStart之前调用,以确保立即有可用的数据提供给audio queue播放。该函数与recording无关
    • Stop(AudioQueueStop)用来重置audio queue,然后停止recording或playback。当playback的callback没有数据来播放时调用该函数
    • Pause(AudioQueuePause)用来暂停recording或playback,不会影响buffers和重置audio queue;调用AudioQueueStart函数恢复
    • Flush(AudioQueueFlush)在最后一个入队的buffer之后调用,用来确保所有的buffer数据,也包括处理中的数据,得到播放或录制
    • Reset(AudioQueueReset)调用该函数会让audio queue立即静音,它会清除掉所有的buffers、重置所有的编解码器和DSP状态

    AudioQueuesStop函数有同步异步两种调用方式:

    • 同步调用会立即停止,不考虑已缓冲的audio data
    • 异步调用会等到队列中所有的buffer全部播放或录完再停止

    八、Audio Queue Parameters

    可以给audio queue设置相关的参数,这些参数通常是针对playback,而不是recording
    有两种方式来设置参数:

    • 对于audio queue,使用AudioQueueSetParameter函数
    • 对于audio queue buffer,使用AudioQueueEnqueueBufferWithParameters函数

    九、录制音频

    下面以录制一个aac音频格式到本地磁盘为例。

    1、创建一个对象LXAudioRecoder来管理audio queue状态、存储dataformat、路径等信息

    static const int kNumberBuffers = 3;
    
    @interface LXAudioRecoder () {
        // 音频队列
        AudioQueueRef               queueRef;
        // buffers数量
        AudioQueueBufferRef         buffers[kNumberBuffers];
        // 音频数据格式
        AudioStreamBasicDescription dataformat;
    }
    
    @property (nonatomic, assign) SInt64 currPacket;
    // 录制的文件
    @property (nonatomic, assign) AudioFileID mAudioFile;
    // 当前录制文件的大小
    @property (nonatomic, assign) UInt32 bufferBytesSize;
    

    2、配置datafromat

        Float64 sampleRate = 44100.0;
        UInt32 channel = 2;
        // 音频格式
        dataformat.mFormatID = kAudioFormatMPEG4AAC;
        // 采样率
        dataformat.mSampleRate = sampleRate;
        // 声道数
        dataformat.mChannelsPerFrame = channel;
        UInt32 formatSize = sizeof(dataformat);
        AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &formatSize, &dataformat);
        // 采样位数
    //    dataformat.mBitsPerChannel = 16;
    //    // 每个包中的字节数
    //    dataformat.mBytesPerPacket = channel * sizeof(SInt16);
    //    // 每个帧中的字节数
    //    dataformat.mBytesPerFrame = channel * sizeof(SInt16);
    //    // 每个包中的帧数
    //    dataformat.mFramesPerPacket = 1;
    //    // flags
    //    dataformat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
    

    3、创建record Audio Queue

    OSStatus status = AudioQueueNewInput(&dataformat, recoderCallBack, (__bridge void *)self, NULL, NULL, 0, &queueRef);
    

    4、编写recoderCallBack

    static void recoderCallBack(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *timestamp, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) {
        LXAudioRecoder *recoder = (__bridge LXAudioRecoder *)aqData;
        
        if (inNumPackets == 0 && recoder->dataformat.mBytesPerPacket != 0) {
            inNumPackets = inBuffer->mAudioDataByteSize / recoder->dataformat.mBytesPerPacket;
        }
        // 将音频数据写入文件
        if (AudioFileWritePackets(recoder.mAudioFile, false, inBuffer->mAudioDataByteSize, inPacketDesc, recoder.currPacket, &inNumPackets, inBuffer->mAudioData) == noErr) {
            recoder.currPacket += inNumPackets;
        }
        if (recoder.isRunning) {
            // 入队
            AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
        }
    }
    

    5、计算Audio Queue Buffers大小

    /** 
     *   获取AudioQueueBuffer大小
     *   seconds:每个buffer保存的音频秒数,一般设置为半秒
     */
    void deriveBufferSize(AudioQueueRef audioQueue, AudioStreamBasicDescription streamDesc, Float64 seconds, UInt32 *outBufferSize) {
        // 音频队列数据大小的上限
        static const int maxBufferSize = 0x50000;
        
        int maxPacketSize = streamDesc.mBytesPerPacket;
        if (maxPacketSize == 0) {  // VBR
            UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
            AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, &maxVBRPacketSize);
        }
        // 获取音频数据大小
        Float64 numBytesForTime = streamDesc.mSampleRate * maxPacketSize * seconds;
        *outBufferSize = (UInt32)(numBytesForTime < maxBufferSize? numBytesForTime : maxBufferSize);
    }
    

    6、创建Audio Queue Buffers

    deriveBufferSize(queueRef, dataformat, 0.5, &_bufferBytesSize);
    // 为Audio Queue准备指定数量的buffer
    for (int i = 0; i < kNumberBuffers; i++) {
        AudioQueueAllocateBuffer(queueRef, self.bufferBytesSize, &buffers[i]);
        AudioQueueEnqueueBuffer(queueRef, buffers[i], 0, NULL);
    }
    

    7、创建音频文件

    NSURL *fileURL = [NSURL URLWithString:filePath];
    AudioFileCreateWithURL((__bridge CFURLRef)fileURL, kAudioFileCAFType, &dataformat, kAudioFileFlags_EraseFile, &_mAudioFile);
    

    8、设置magic cookie for an audio file
    默写压缩音频格式,如MPEG 4 AAC,使用一个结构来包含audio的元数据。这种结构叫做magic cookie。当使用audio queue services录制这种格式时,你必须从audio queue中获取magic cookie并在开始录制前添加到音频文件中。
    注意:下面方法需在recording之前和在停止recording时调用,因为某些编解码器在recording停止时更新magic cookie数据

    - (OSStatus)setupMagicCookie {
        UInt32 cookieSize;
        OSStatus status = noErr;
        if (AudioQueueGetPropertySize(queueRef, kAudioQueueProperty_MagicCookie, &cookieSize) == noErr) {
            char *magicCookie = (char *)malloc(cookieSize);
            if (AudioQueueGetProperty(queueRef, kAudioQueueProperty_MagicCookie, magicCookie, &cookieSize) == noErr) {
                status = AudioFileSetProperty(_mAudioFile, kAudioFilePropertyMagicCookieData, cookieSize, magicCookie);
            }
            free(magicCookie);
        }
        return status;
    }
    

    9、record audio

    - (void)recoder {
        
        if (self.isRunning) {
            return;
        }
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
        [[AVAudioSession sharedInstance] setActive:true error:nil];
        
        OSStatus status = AudioQueueStart(queueRef, NULL);
        if (status != noErr) {
            NSLog(@"start queue failure");
            return;
        }
        _isRunning = true;
    }
    
    - (void)stop {
        if (self.isRunning) {
            AudioQueueStop(queueRef, true);
            _isRunning = false;
            
            [[AVAudioSession sharedInstance] setActive:false withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
        }
    }
    

    10、clean up

    - (void)dealloc {
        AudioQueueDispose(queueRef, true);
        AudioFileClose(_mAudioFile);
    }
    

    十、播放音频

    以播放本地音频文件为例
    1、创建一个对象用来管理audio queue状态、datafromat、buffers等

    static const int kNumberBuffers = 3;
    
    @interface LXAudioPlayer () {
        AudioStreamBasicDescription   dataFormat;
        AudioQueueRef                 queueRef;
        AudioQueueBufferRef           mBuffers[kNumberBuffers];
    }
    
    @property (nonatomic, assign) AudioFileID mAudioFile;
    @property (nonatomic, assign) UInt32 bufferByteSize;
    @property (nonatomic, assign) SInt64 mCurrentPacket;
    @property (nonatomic, assign) UInt32 mPacketsToRead;
    @property (nonatomic, assign) AudioStreamPacketDescription *mPacketDescs;
    @property (nonatomic, assign) bool isRunning;
    @end
    

    2、打开文件

    NSURL *fileURL = [NSURL URLWithString:filePath];
    OSStatus status = AudioFileOpenURL((__bridge CFURLRef)fileURL, kAudioFileReadPermission, kAudioFileCAFType, &_mAudioFile);
    

    3、获取文件格式

    // 获取文件格式
    UInt32 dataFromatSize = sizeof(dataFormat);
    AudioFileGetProperty(_mAudioFile, kAudioFilePropertyDataFormat, &dataFromatSize, &dataFormat);
    

    4、创建playback audio queue

    // 创建播放音频队列
    AudioQueueNewOutput(&dataFormat, playCallback, (__bridge void *)self, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &queueRef);
    

    5、编写play callback

    static void playCallback(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
        LXAudioPlayer *player = (__bridge LXAudioPlayer *)aqData;
        
        UInt32 numBytesReadFromFile = player.bufferByteSize;
        UInt32 numPackets = player.mPacketsToRead;
        AudioFileReadPacketData(player.mAudioFile, false, &numBytesReadFromFile, player.mPacketDescs, player.mCurrentPacket, &numPackets, inBuffer->mAudioData);
        if (numPackets > 0) {
            inBuffer->mAudioDataByteSize = numBytesReadFromFile;
            player.mCurrentPacket += numPackets;
            AudioQueueEnqueueBuffer(player->queueRef, inBuffer, player.mPacketDescs ? numPackets : 0, player.mPacketDescs);
        } else {
            NSLog(@"play end");
            AudioQueueStop(player->queueRef, false);
            player.isRunning = false;
        }
    }
    

    6、计算buffer的大小

    void playBufferSize(AudioStreamBasicDescription basicDesc, UInt32 maxPacketSize, Float64 seconds, UInt32 *outBufferSize, UInt32 *outNumPacketsToRead) {
        static const int maxBufferSize = 0x50000;
        static const int minBufferSize = 0x4000;
        
        if (basicDesc.mFramesPerPacket != 0) {
            Float64 numPacketsForTime = basicDesc.mSampleRate / basicDesc.mFramesPerPacket * seconds;
            *outBufferSize = numPacketsForTime * maxPacketSize;
        } else {
            *outBufferSize = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize;
        }
        
        if (*outBufferSize > maxBufferSize && *outBufferSize > maxPacketSize) {
            *outBufferSize = maxBufferSize;
        } else {
            if (*outBufferSize < minBufferSize) {
                *outBufferSize = minBufferSize;
            }
        }
        *outNumPacketsToRead = *outBufferSize / maxPacketSize;
    }
    

    7、为数据包描述分配内存

    bool isFormatVBR = dataFormat.mBytesPerPacket == 0 || dataFormat.mFramesPerPacket == 0;
    if (isFormatVBR) {
        _mPacketDescs = (AudioStreamPacketDescription *)malloc(_mPacketsToRead * sizeof(AudioStreamPacketDescription));
    } else {
        _mPacketDescs = NULL;
    }
    

    8、set magic cookie

    - (void)setupMagicCookie {
        // magic cookie
        UInt32 cookieSize = sizeof(UInt32);
        if (AudioFileGetPropertyInfo(_mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, NULL) == noErr && cookieSize) {
            char *magicCookie = (char *)malloc(cookieSize);
            if (AudioFileGetProperty(_mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, magicCookie) == noErr) {
                AudioQueueSetProperty(queueRef, kAudioQueueProperty_MagicCookie, magicCookie, cookieSize);
            }
            free(magicCookie);
        }
    }
    

    9、创建buffer

    UInt32 maxPacketSize;
    UInt32 propertySize = sizeof(maxPacketSize);
    AudioFileGetProperty(_mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize);
    playBufferSize(dataFormat, maxPacketSize, 0.5, &_bufferByteSize, &_mPacketsToRead);
    // 分配音频队列
    for (int i = 0; i < kNumberBuffers; i++) {
        AudioQueueAllocateBuffer(queueRef, _bufferByteSize, &mBuffers[i]);
        playCallback((__bridge void *)self, queueRef, mBuffers[i]);
    }
    
    

    10、play audio

    - (void)play {
        if (self.isRunning) {
            return;
        }
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
        //[[AVAudioSession sharedInstance] setActive:YES error:nil];
        [[AVAudioSession sharedInstance] setActive:true withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
        
        OSStatus status = AudioQueueStart(queueRef, NULL);
        if (status != noErr) {
            NSLog(@"play error");
            return;
        }
        self.isRunning = true;
    }
    

    11、stop play

    - (void)stop {
        if (self.isRunning) {
            self.isRunning = false;
            AudioQueueStop(queueRef, true);
            
            [[AVAudioSession sharedInstance] setActive:false withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
        }
    }
    

    12、clean

    - (void)dealloc {
        AudioFileClose(_mAudioFile);
        AudioQueueDispose(queueRef, true);
        if (_mPacketDescs) {
            free(_mPacketDescs);
        }
    }
    

    播放和录音demo已上传Github
    参考文章:
    1、Audio Queue Services Programing Guide

    相关文章

      网友评论

        本文标题:Audio Queue详解

        本文链接:https://www.haomeiwen.com/subject/jmllyctx.html