使用Audio queue实现录音

    前几篇文章提到过使用audiotool来实现录音,其中所使用的就是audio queue来具体的实现其中的相关功能。这篇文章就详细的讲一下,如何使用audio queue来进行录音。

其实说起来很简单,总共有七步:

    1.自定义一个结构体来管理录音的状态,音频的格式、路径等各种信息。

    2.写一个audio queue的回调函数来实现真正的录音。

    3.写代码来计算出audio queue的缓存的大小、处理音频的magic cookie。

    4.填充我们自定义的结构体.

    5.创建一个录音的audio queue并且让他创建一系列的缓存.并且创建一个因为文件来写入。

    6.告诉audio queue开始录音。

    7.录音结束后,告诉audio queue停止,并且清理他的缓存。

下面我们就结合苹果的官方示例SpeakHere来一步一步的讲解和实现:

1.自定义一个结构体。

#definekNumberRecordBuffers3//1

class AQRecorder

{

    UInt64startTime;

    private:

    CFStringRefmFileName;//2

    AudioQueueRefmQueue;//3

    AudioQueueBufferRefmBuffers[kNumberRecordBuffers];//4

    AudioFileIDmRecordFile;//5

    SInt64mRecordPacket;//6

    CAStreamBasicDescriptionmRecordFormat;//7

    BooleanmIsRunning;//8

};

下面详细解释一下各个字段的含义:

    1.设置audio queue的缓存的大小

    2.录音文件的名称

    3.App所创建的录音audioqueue

    4.Audio queue的缓存数组

    5.你录制的音频所要写入的文件对象

    6.当前audio queue的缓存所要写入文件的第一个包的序号

    7.AudioStreamBasicDescription表示音频文件的各种参数和格式,他会在mQueue中被使用到。

    8.表示是否正在录音。

2.写一个audio queue的回调函数来实现真正的录音。

    这个回调做两件事情:

    · 将已经被写满的audio queue的缓存数据写到相应的录音文件中

    · 将这个刚刚写入到文件中的audio queue的缓存返回给audio queue的缓存数组

    录音audio queue的回调的定义:

    void AQRecorder::MyInputBufferHandler(void *inUserData,\\1

        AudioQueueRefinAQ,\\2

        AudioQueueBufferRefinBuffer,\\3

        constAudioTimeStamp *inStartTime,\\4

        UInt32inNumPackets,\\5

        constAudioStreamPacketDescription*inPacketDesc)\\6

其中的参数含义:

    1.通常他就是我们定义的这个录音的类AQRecorder

    2.拥有这个回调的audio queue

    3.这个回调所对应的audio queue的缓存,包含着已经录制好的音频

    4.缓存中的音频数据的抽样时间

    5.在参数inPacketDesc中描述的包的数量

    6.对于压缩的音频数据保存时需要AudioStreamPacketDescription,这个数据结构是由缓存中的编码器来提供的。

将audioqueue的一个缓存数据保存到文件中

    录音audio queue的第一个任务就是将缓存audio queue的缓存数据写入到磁盘中。这个缓存中的数据就是从音频输入设备刚刚获取到的并写入到缓存中的。这个回调中会使用AudioFileWritePackets这个方法,这个方法的定义如下:

    AudioFileWritePackets (// 1

        pAqData->mRecordFile,// 2

        false,// 3

        inBuffer->mAudioDataByteSize,// 4

        inPacketDesc,// 5

        pAqData->mCurrentPacket,//6

        &inNumPackets,// 7

        inBuffer->mAudioData// 8

    );

     1. 这个方法将缓存中的数据写入到磁盘中

     2. 这个表示将要写入的音频文件对象。

    3. False表示不需要缓存需要写入的数据

    4. 需要写入的音频数据的大小。inBuffer表示的是audioqueue回调中传入的缓存对象。

    5. 一个数组,数组中的每个元素是对每个音频数据包的描述。NULL意味着不需要包描述(对于CBR来说就是不需要的)

    6. 需要写入的第一个包的序号

    7. 需要写入的包的数量

    8.需要写入单磁盘上的新的音频数据

将Audio Queue的缓存重新返回给audio queue

    现在缓存中所有的音频数据已经写入到磁盘中,需要将缓存重新传递给audio queue。这样audio queue就可以继续使用他来接收新的数据。

    AudioQueueEnqueueBuffer(// 1

        pAqData->mQueue,// 2

        inBuffer,// 3

        0,// 4

        NULL// 5

    );

    1. AudioQueueEnqueueBuffer将一个audio queue的缓存加入到audio queue的缓存队列中

    2.缓存将要加入到的audio queue

    3. 将要返回的缓存

    4. audio queue的缓存数据中的包描述的数量,设置为0是因为他在录音中是没有用到的。

    5. 包描述的数组,用来描述录音数据的audio queue缓存数据的。NULL是因为他在录音中没有被用到。

一个完整的录音audio queue的回调的实现

// AudioQueue callback function, called when an input buffers hasbeen filled.

void AQRecorder::MyInputBufferHandler(void *inUserData,

    AudioQueueRefinAQ,

    AudioQueueBufferRefinBuffer,

    constAudioTimeStamp *inStartTime,

    UInt32inNumPackets,

    constAudioStreamPacketDescription*inPacketDesc)

{

    AQRecorder *aqr =(AQRecorder *)inUserData;

    try {

        if(inNumPackets > 0) {

        // writepackets to file

        XThrowIfError(AudioFileWritePackets(aqr->mRecordFile,FALSE, inBuffer->mAudioDataByteSize,

inPacketDesc, aqr->mRecordPacket,&inNumPackets, inBuffer->mAudioData),

"AudioFileWritePackets failed");

        aqr->mRecordPacket+= inNumPackets;

    }

    // if we're notstopping, re-enqueue the buffe so that it gets filled again

    if(aqr->IsRunning())

        XThrowIfError(AudioQueueEnqueueBuffer(inAQ,inBuffer, 0, NULL), "AudioQueueEnqueueBuffer failed");

    } catch (CAXExceptione) {

    char buf[256];

    fprintf(stderr,"Error: %s (%s)\n", e.mOperation, e.FormatError(buf));

}

}

    1. 获取我们传递进去的类

    2. 如果需要写入的包的数量超过0,将包的数据写入到文件中

    3. 如果写入成功,增加已经写入的音频包的序号

    4. 如果audio queue正在运行中,将缓存交还给audio queue加入到缓存队列中

写一个函数来计算audio queue的缓存的大小

    Audio Queue服务希望你的应用可以提供一个你需要使用的audio queue的缓存的大小。下面的方法提供了如何来提供一个足够大的缓存来保存指定时间长度内的音频数据。计算的时候需要考虑你所录制的音频的格式。它包含了所有能够影响大小的因素,包括音频通道的数量等等。

int AQRecorder::ComputeRecordBufferSize(constAudioStreamBasicDescription *format, float seconds)

{

    int packets, frames,bytes = 0;

    try {

        frames =(int)ceil(seconds * format->mSampleRate);

        if(format->mBytesPerFrame > 0)

            bytes =frames * format->mBytesPerFrame;

        else {

            UInt32maxPacketSize;

            if(format->mBytesPerPacket > 0)

                maxPacketSize= format->mBytesPerPacket;//constant packet size

        else {

            UInt32propertySize = sizeof(maxPacketSize);

            XThrowIfError(AudioQueueGetProperty(mQueue,kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize,

            &propertySize), "couldn't get queue'smaximum output packet size");

        }

        if(format->mFramesPerPacket > 0)

            packets= frames / format->mFramesPerPacket;

        else

            packets= frames;// worst-case scenario: 1frame in a packet

        if(packets == 0)// sanitycheck

            packets= 1;

        bytes =packets * maxPacketSize;

        }

    } catch (CAXExceptione) {

        char buf[256];

        fprintf(stderr,"Error: %s (%s)\n", e.mOperation, e.FormatError(buf));

        return 0;

    }

    return bytes;

}

计算的主要方式是:

    1. 首先设定每个缓存需要存储的音频的长度。

    2. 计算给定的时间内音频所占用的空间的计算方法:

        a.如果可以获取到帧的大小:

            时间*每秒钟的采样率*每一帧的大小,在苹果的文档中,将mSampleRate就定义为每秒钟采样到的帧的数目

        b.如果可以获取到package大小:

            时间*(每秒钟的采样率/每package中Frame的数量)*每package的大小

        c.以上的这些方法对于CRV是使用的,因为每个package中的frame数量,每个frame的大小都是相同的。但是对于其他的可能就有问题,就需要采用如下的形式获取。如果获取不到package的大小:

            通过AudioQueueGetProperty获取他的kAudioQueueProperty_MaximumOutputPacketSize得大小,然后使用公式:

            时间*(每秒钟的采样率/每package中Frame的数量)*每package的最大的大小

        d.实际上还有一种方式来计算规定的时间内录制的音频的数据大小:

            时间*采样率*采样声道数*声道的采样位数

给音频文件增加MagicCookies

    一些压缩的音频格式,例如MPEG 4 AAC会使用一些结构来保存文件信息保存在文件头中,这些结构被叫做magic cookies当你使用audio queue服务来录制音频的时候,你必须在录制开始之前从audio queue中获取这些信息,并且把他加到文件头中。

    下面展示了如何获取magic cookie并把他加入到文件中,并且在录制结束以后,你也可能需要更新magic cookie中的信息。

OSStatusSetMagicCookieForFile (

    AudioQueueRef inQueue,// 1

    AudioFileIDinFile// 2

) {

    OSStatus result = noErr;// 3

    UInt32 cookieSize;// 4

    if (

        AudioQueueGetPropertySize (// 5

            inQueue,

            kAudioQueueProperty_MagicCookie,

            &cookieSize

        ) == noErr

    ) {

            char* magicCookie =

            (char *) malloc (cookieSize);// 6

        if (

            AudioQueueGetProperty (// 7

                inQueue,

                kAudioQueueProperty_MagicCookie,

                magicCookie,

                &cookieSize

            ) == noErr

        )

        result =AudioFileSetProperty (// 8

            inFile,

            kAudioFilePropertyMagicCookieData,

            cookieSize,

            magicCookie

        );

        free (magicCookie);// 9

    }

    return result;// 10

}

    1.录音所使用的audio queue.

    2.音频要写入的文件.

    3. 一个返回值,代表着写入是否成功.

    4. 持有magic cookie数据大小的变量.

    5.从audio queue获取magic cookie的大小并保存到变量cookieSize.

    6. 开辟内存空间来持有magic cookie信息.

    7. 通过查询audio queue的kAudioQueueProperty_MagicCookie属性来获得magic cookie.

    8. 将magic cookie写入到你的录音文件中.AudioFileSetProperty方法在AudioFile.h文件中声明.

    9. 释放cookie变量所占有的控件.

    10. 返回这个方法是否成功.

设置录音的音频格式

    这一部分描述你如何为audio queue设置录音的音频格式。这个audio queue使用这个设置来录进音频。为了设置音频格式,你需要设置:

    · 音频数据的格式(例如linear PCM, AAC,等等.)

    · 采样频率(例如:44.1 kHz)

    · 音频的声道数量(例如对于多媒体是2)

    · 位深(例如16bits)

    · 每一个package所包含的Fram数量(对于linear PCM,每一个package里面包含一个frame)

    · 音频文件的格式(例如CAF, AIFF等)

    · 其他的音频文件格式所需要的音频设置

    下面设置了一个音频的属性来进行录音。在实际的代码中,你可能需要允许用户来设置其中的很多属性,而不是写死在代码里面。两种方式都是为了填充mDataFormat的AQRecorderState字段。

    AQRecorderStateaqData;// 1

    aqData.mDataFormat.mFormatID= kAudioFormatLinearPCM; // 2

    aqData.mDataFormat.mSampleRate= 44100.0;// 3

    aqData.mDataFormat.mChannelsPerFrame= 2;// 4

    aqData.mDataFormat.mBitsPerChannel= 16;// 5

    aqData.mDataFormat.mBytesPerPacket=// 6

    aqData.mDataFormat.mBytesPerFrame=

    aqData.mDataFormat.mChannelsPerFrame *sizeof (SInt16);

    aqData.mDataFormat.mFramesPerPacket= 1;// 7

    AudioFileTypeIDfileType=kAudioFileAIFFType;// 8

    aqData.mDataFormat.mFormatFlags=// 9

        kLinearPCMFormatFlagIsBigEndian

        | kLinearPCMFormatFlagIsSignedInteger

        | kLinearPCMFormatFlagIsPacked;

    1. 创建自定义的结构体AQRecorderState的实例,他的mDataFormat中包含AudioStreamBasicDescription结构体.mDataFormat为audio queue提供了一个初始的音频文件格式的设置和音频文件的保存格式的设置.

    2.设置音频的格式为linear PCM.

    3.设置采样率为44.1 kHz.

    4.设置声道的数目为2.

    5.设置每个声道的位深为16.

    6.设置每个package的大小,每个frame的大小为4(2个声道乘以每个采样的位深2).

    7. 设置每个package中包含的frame的数量为1.

    8. 设置文件类型为AIFF.

    9. 为每个特定的文件类型设置特定的参数

创建一个录音audioqueue

现在设置完录音的回调和音频的格式,你可以创建并设置一个audio queue。

创建一个audioqueue

    AudioQueueNewInput(// 1

        &aqData.mDataFormat,// 2

        HandleInputBuffer,// 3

        &aqData,// 4

        NULL,// 5

        kCFRunLoopCommonModes,// 6

        0,//7

        &aqData.mQueue// 8

);

    1. AudioQueueNewInput方法创建一个新的录音audioqueue.

    2.所需要录制的音频数据格式

    3. 录音audio queue所使用的回调

    4. 录音audio queue所使用的自定义结构体。

    5. 这个回调会在哪个runloop上被回调.NULL表示默认会在和audio queue同一个线程中被回调.通常我们都使用这个设置—它允许你的audio queue录音的同时,用户可以手动的在界面上停止录音

    6. 回调所在的runloop的模式.通常使用kCFRunLoopCommonModes

    7. 保留.必须是0.

    8. 新创建的audio queue.

从audio queue获取完整的音频格式

    当audio queue已经被创建,通常他都会比你填充AudioStreamBasicDescription中更多的参数。为了获取参数,你可以调用AudioQueueGetProperty,

    UInt32dataFormatSize = sizeof (aqData.mDataFormat);// 1

    AudioQueueGetProperty(// 2

    aqData.mQueue,// 3

    kAudioQueueProperty_StreamDescription,// 4

    &aqData.mDataFormat,// 5

    &dataFormatSize// 6

);

    1. 从audio queue中获取你想要知道的属性的大小

    2. AudioQueueGetProperty方法从audio queue中获取你想要知道的特定的属性的值

    3. 你想要获取属性的audio queue.

    4. 你想要获取的属性的ID.

    5. 从audio queue获取的完整的音频格式,以AudioStreamBasicDescription格式输出

    6. 你所想要获取的AudioStreamBasicDescription结构体的大小.

创建一个音频文件

创建并设置完audio queue之后,你需要创建一个音频文件将音频写入进去。这个文件会使用你前面的各种设置来创建。

CFURLRefaudioFileURL =

    CFURLCreateFromFileSystemRepresentation(// 1

        NULL,//2

        (const UInt8 *) filePath,// 3

        strlen (filePath),// 4

        false//5

    );

    AudioFileCreateWithURL(// 6

        audioFileURL,// 7

        fileType,// 8

        &aqData.mDataFormat,// 9

        kAudioFileFlags_EraseFile,// 10

        &aqData.mAudioFile// 11

    );

    1. CFURLCreateFromFileSystemRepresentation方法创建一个CFURL object表示你将要录制的文件

    2. 使用NULL来使用默认的内存分配方式.

    3. 你希望创建的文件的路径.

    4. 文件路径的长度.

    5.False表示filePath是一个文件而不是目录

    6. AudioFileCreateWithURL方法创建一个新的音频文件如果已经存在就初始化已经存在的文件.

    7. 所需要创建的新的音频文件的URL.这个URL可以从第一步的CFURLCreateFromFileSystemRepresentation中获取到.

    8. 新文件的文件格式.在这个例子中已经在前面的代码中被设置为kAudioFileAIFFType

    9.一个AudioStreamBasicDescription结构体,表示的你你将要写入的音频的格式.

    10. 如果这个文件已经存在,就删除他.

    11. 音频文件对象,你将要录制的

设置audioqueue的缓存大小

    你可以使用我们上文提到的方法ComputeRecordBufferSize来计算audio queue的缓存大小。

ComputeRecordBufferSize(// 1

    aqData.mQueue,// 2

    aqData.mDataFormat,// 3

    0.5,// 4

    &aqData.bufferByteSize// 5

);

    1. ComputeRecordBufferSize方法来计算缓存的大小.

    2. 你所要设置的audio queue.

    3. 你所要设置的音频格式

    4. 每一个音频缓存所应该保存的音频的长度。0.5是一个通常很好的值。

    5. 每一个audio queue的缓存的大小

准备一系列的缓存

    现在你告诉audio queue你已经准备好创建一系列的缓存。

    for(int i = 0; i < kNumberBuffers; ++i) {// 1

        AudioQueueAllocateBuffer (// 2

        aqData.mQueue,// 3

        aqData.bufferByteSize,// 4

        &aqData.mBuffers[i]// 5

    );

    AudioQueueEnqueueBuffer (// 6

        aqData.mQueue,// 7

        aqData.mBuffers[i],// 8

        0,// 9

        NULL// 10

    );

}

    1.[endif]循环的给每一个audio queue的缓存,分配空间并插入到缓存队列中.

    2. AudioQueueAllocateBuffer告诉audio queue来分配一个audio queue的缓存控件.

    3. 需要这个空间的audio queue.

    4. 需要创建的缓存的大小

    5. 新创建的缓存

    6. AudioQueueEnqueueBuffer将缓存插入到缓存队列的尾部

    7. 你所要操作的audio queue.

    8. 你想要插入的audio queue缓存

    9. 在创建录音缓存的时候无用.

    10. 在创建录音缓存的时候无用.

录制音频

    上面准备的代码可以让你很轻松的录制音频。

    aqData.mCurrentPacket= 0;// 1

    aqData.mIsRunning= true;// 2

    AudioQueueStart(// 3

        aqData.mQueue,// 4

        NULL//5

    );

    //Wait, on user interface thread, until user stops the recording

    AudioQueueStop(//6

        aqData.mQueue,// 7

        true//8

    );

    aqData.mIsRunning= false;// 9

    1 初始化包的序列号为0来开始录制一个音频文件

    2. 设置一个标志位来表示正在录音.这个会在回调中被使用

    3. AudioQueueStart在自己的线程中开始录音

    4. 所要开始的audio queue.

    5. NULL说明audio queue应该立即开始录音.

    6. AudioQueueStop停止并重置audioqueue.

    7. 要停止的audio queue.

    8. True表示同步停止

    9. 设置标志位表示录制已经结束.

录音完成之后的清理

    AudioQueueDispose(// 1

        aqData.mQueue,// 2

        true//3

    );

    AudioFileClose(aqData.mAudioFile);// 4

    1. AudioQueueDispose销毁audio queue和他的资源包括各种缓存

    2. 你要销毁的audio queue.

    3. True同步的销毁.

    4. 关闭录制的音频文件。

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

推荐阅读更多精彩内容