几种播放音频文件的方式(六) —— 音频队列服务(Audio Queue Services)之关于音频队列(三)

版本记录

版本号 时间
V1.0 2017.12.27

前言

ios系统中有很多方式可以播放音频文件,这里我们就详细的说明下播放音乐文件的原理和实例。感兴趣的可以看我写的上面几篇。
1. 几种播放音频文件的方式(一) —— 播放本地音乐
2. 几种播放音频文件的方式(二) —— 音效播放
3. 几种播放音频文件的方式(三) —— 网络音乐播放
4. 几种播放音频文件的方式(四) —— 音频队列服务(Audio Queue Services)(一)
5. 几种播放音频文件的方式(五) —— 音频队列服务(Audio Queue Services)简介(二)

About Audio Queues - 关于音频队列

在本章中,您将了解音频队列的功能,体系结构和内部工作原理。 您将了解音频队列audio queues,音频队列缓冲区audio queue buffers以及音频队列用于录制或回放的回调函数callback。 您还可以了解有关音频队列状态和参数的信息。 到本章结束时,您将获得有效使用此技术所需的概念性理解。


What Is an Audio Queue? - 音频队列是什么?

音频队列Audio Queue是用于在iOS或Mac OS X中录制或播放音频的软件对象。它由AudioQueue.h头文件中声明的AudioQueueRef不透明数据类型表示。

音频队列的工作是:

  • 连接到音频硬件
  • 管理内存
  • 根据需要使用编解码器压缩音频格式
  • 调解录制或播放

您可以将音频队列与其他Core Audio接口以及相对较少量的自定义代码一起使用,以在应用程序中创建完整的数字音频录制或播放解决方案。


Audio Queue Architecture - 音频队列结构

下面我们就看一下音频队列的结构。

所有音频队列具有相同的总体结构,由以下部分组成:

  • 一组音频队列缓冲区audio queue buffers,每个缓冲区都是一些音频数据的临时存储库
  • 缓冲队列buffer queue,音频队列缓冲区audio queue buffers的有序列表
  • 一个你写的音频队列回调函数audio queue callback

结构的改变取决于音频队列是用于记录还是回放。 不同点在于音频队列如何连接其输入和输出,以及回调函数的作用。

Audio Queues for Recording - 音频队列用于记录

使用AudioQueueNewInput函数创建的录制音频队列的结构如图1-1所示。

Figure 1-1 A recording audio queue

记录音频队列的输入侧通常连接到外部音频硬件,如麦克风。例如,在iOS中,音频来自用户内置麦克风或耳机麦克风所连接的设备。在Mac OS X的默认情况下,音频来自系统偏好设置中由用户设置的系统的默认音频输入设备。

录制音频队列的输出端使用您编写的回调函数。在录制到磁盘时,回调会将新音频数据的缓冲区(从音频队列接收)写入音频文件。但是,记录音频队列可以以其他方式使用。例如,您的回调可以将音频数据直接提供给您的应用程序,而不是将其写入磁盘。

您将在 The Recording Audio Queue Callback Function中了解有关此回调的更多信息。

每个音频队列(无论是用于记录还是播放)都有一个或多个音频队列缓冲区audio queue buffers。这些缓冲区按照一个称为缓冲队列buffer queue的特定顺序排列。在图中,音频队列缓冲区按照它们被填充的顺序进行编号 - 这与它们被交给回调的顺序是相同的。您将了解音频队列如何在The Buffer Queue and Enqueuing中使用其缓冲区buffers

Audio Queues for Playback - 音频队列回放

播放音频队列(使用AudioQueueNewOutput函数创建)的结构如图1-2所示。

Figure 1-2 A playback audio queue

在回放音频队列中,回调位于输入端。 该回调负责从磁盘(或其他来源)获取音频数据,并将其交给音频队列。 回放回调还会告诉音频队列在没有更多数据可以播放时停止播放。 您将在The Playback Audio Queue Callback Function中了解有关此回调的更多信息。

回放音频队列的输出通常连接到外部音频硬件,如扬声器。 在iOS中,音频会转到用户选择的设备上,例如接收器或耳机。 在Mac OS X的默认情况下,音频将转到系统偏好设置中由用户设置的系统的默认音频输出设备。


Audio Queue Buffers - 音频队列缓冲

音频队列缓冲区是AudioQueue.h头文件中声明的AudioQueueBuffer类型的数据结构:

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

在代码清单中突出显示的mAudioData字段指向缓冲区本身:一块内存,用作正在播放或录音的音频数据的瞬时块的容器。其他字段中的信息有助于音频队列audio queue管理缓冲区buffer

音频队列可以使用任意数量的缓冲区。你的应用程序指定多少。一个典型的数字是三个。这使得一个用于写入磁盘,而另一个正在填充新的音频数据。如果需要补偿磁盘I / O延迟等问题,那么第三个缓冲区就有用了。Figure 1-3说明了这一点。

音频队列为其缓冲区执行内存管理。

这提高了您添加到应用程序的录制和播放功能的稳健性。它还有助于优化资源使用。

有关AudioQueueBuffer数据结构的完整说明,请参阅Audio Queue Services Reference


The Buffer Queue and Enqueuing - 缓冲队列和入队

缓冲队列就是给音频队列,实际上是音频队列服务,他们的名字的对象。 您在Audio Queue Architecture中认识了缓冲区队列 —— 一个有序的缓冲区列表。 在这里,您将了解到音频队列对象与回调函数在录制或回放过程中如何管理缓冲区队列。 尤其是,您了解入队enqueuing,将音频队列缓冲区添加到缓冲区队列。 无论您正在执行录制还是回放,入队都是回调执行的任务。

1. The Recording Process - 录音过程

录制时,一个音频队列缓冲区正在填充从输入设备(如麦克风)获取的音频数据。 缓冲区队列中剩余的缓冲区排列在当前缓冲区后面,等待依次填充音频数据。

音频队列按已获取音频数据的填充缓冲区的顺序将它们传递给您的回调。 图1-3说明了使用音频队列时录制的工作原理。

Figure 1-3 The recording process

在图1-3的步骤1中,开始记录。 音频队列用获取的数据填充缓冲区。

在第2步中,第一个缓冲区已被填充。 音频队列调用回调函数,将其交给完整的缓冲区(缓冲区1)。 回调(步骤3)将缓冲区的内容写入音频文件。 同时,音频队列用新获取的数据填充另一个缓冲区(缓冲区2)。

在第4步中,回调将刚刚写入磁盘的缓冲区(缓冲区1)排入队列,以便再次进行填充。 音频队列再次调用回调(步骤5),把下一个完整的缓冲区(缓冲区2)交给它。 回调(第6步)将此缓冲区的内容写入音频文件。 这种循环稳定状态一直持续到用户停止记录。

2. The Playback Process - 播放过程

播放时,一个音频队列缓冲区正在发送到输出设备,如扬声器。 缓冲区队列中剩余的缓冲区排列在当前缓冲区后面,等待依次播放。

音频队列按照播放的顺序将播放的音频数据缓冲区传递到您的回调。 回调会将新的音频数据读入缓冲区,然后将其排入队列。 图1-4说明了使用音频队列时的播放方式。

Figure 1-4 The playback process

在图1-4的步骤1中,应用程序启动播放音频队列。 应用程序为每个音频队列缓冲区调用一次回调函数,将其填充并添加到缓冲区队列中。 启动确保播放可以在您的应用程序调用AudioQueueStart函数时立即启动(步骤2)。

在步骤3中,音频队列发送第一个缓冲区(缓冲区1)输出。

一旦第一个缓冲器被播放,回放音频队列就进入循环稳定状态。 音频队列开始播放下一个缓冲区(缓冲区2,步骤4)并调用回调(步骤5),将刚刚播放的缓冲区(缓冲区1)传送给它。 回调(第6步)从音频文件中填充缓冲区,然后将其排入队列进行回放。

3. Controlling the Playback Process - 控制播放过程

音频队列缓冲区总是以它们入队的顺序播放。 但是,音频队列服务使用AudioQueueEnqueueBufferWithParameters函数为播放过程提供了一些控制。 这个功能可以让你:

  • 为缓冲区设置精确的播放时间。 这可以让你支持同步。
  • 修剪音频队列缓冲区开始或结束处的帧。 这可以让你删除前导或尾随静默。
  • 以缓冲区的粒度设置回放增益。

有关设置回放增益的更多信息,请参阅Audio Queue Parameters。 有关AudioQueueEnqueueBufferWithParameters函数的完整说明,请参阅 Audio Queue Services Reference


The Audio Queue Callback Function - 音频队列回调函数

通常,使用音频队列服务的大部分编程工作包括编写音频队列回调函数。

在录制或回放过程中,音频队列回调函数由拥有它的音频队列重复调用。 两次调用之间的时间取决于音频队列缓冲区的容量,一般范围从半秒到几秒。

无论是用于记录还是回放,音频队列回调的一个责任是将音频队列缓冲audio queue buffers返回到缓冲区队列buffer queue。 回调函数使用AudioQueueEnqueueBuffer函数将缓冲区添加到缓冲区队列的末尾。 对于播放,如果需要更多控制,则可以使用AudioQueueEnqueueBufferWithParameters函数,如Controlling the Playback Process中所述。

1. The Recording Audio Queue Callback Function - 录制音频队列回调函数

本节介绍将音频录制到磁盘文件的常见情况而写的回调。 以下是AudioQueue.h头文件中声明的录音音频队列回调的原型:

AudioQueueInputCallback (
    void                               *inUserData,
    AudioQueueRef                      inAQ,
    AudioQueueBufferRef                inBuffer,
    const AudioTimeStamp               *inStartTime,
    UInt32                             inNumberPacketDescriptions,
    const AudioStreamPacketDescription *inPacketDescs
);

记录音频队列在调用回调函数时提供了回调函数将下一组音频数据写入音频文件的所有内容:

  • inUserData,通常情况下,你已经设置了以包含音频队列代表你对文件的写入状态信息和其缓冲区,音频文件对象(类型AudioFileID的)的自定义结构,以及音频数据格式的信息文件。
  • inAQ是调用回调的音频队列。
  • inBuffer是一个音频队列缓冲区,由音频队列新填充,包含回调函数需要写入磁盘的新数据。数据已根据您在自定义结构中指定的格式(在inUserData参数中传递)格式化。有关详情,请参阅Using Codecs and Audio Data Formats
  • inStartTime是缓冲区中第一个采样的采样时间。对于基本录音,你的callback不使用此参数。
  • inNumberPacketDescriptionsinPacketDescs参数中的数据包描述的数量。如果您正在录制为VBR(可变比特率)格式,则音频队列会将此参数的值提供给您的callback,然后将该值传递AudioFileWritePackets函数。 CBR(恒定比特率)格式不使用数据包描述。对于CBR记录,音频队列将此设置和inPacketDescs参数设置为NULL
  • inPacketDescs是与缓冲区中的样本对应的一组数据描述。同样,音频队列为此参数提供值,如果音频数据是在VBR的格式,你的callback将其传递到AudioFileWritePackets函数(在AudioFile.h头文件中声明)。

有关录音callback的更多信息,请参阅本文档中的Recording Audio,并参阅Audio Queue Services Reference

2. The Playback Audio Queue Callback Function - 播放音频回调函数

本节介绍您为从磁盘文件播放音频的常见情况而写的回调。 以下是AudioQueue.h头文件中声明的回放音频队列回调的原型:

AudioQueueOutputCallback (
    void                  *inUserData,
    AudioQueueRef         inAQ,
    AudioQueueBufferRef   inBuffer
);

回放音频队列在调用回调函数时提供回调需要从音频文件读取下一组音频数据的内容:

  • inUserData,通常情况下,你已经设置了以包含音频队列代表你对文件的写入状态信息和其缓冲区,音频文件对象(类型AudioFileID的)的自定义结构,以及音频数据格式的信息文件。在播放音频队列的情况下,您的回调会使用此结构中的字段跟踪当前数据包索引。
  • inAQ是调用callback的音频队列。
  • inBuffer是音频队列缓冲区,由音频队列提供,您的回调函数将填充从正在播放的文件中读取的下一组数据。

如果您的应用程序正在播放VBR数据,则回调callback需要获取正在读取的音频数据的数据包信息。它通过调用AudioFile.h头文件中声明AudioFileReadPackets函数来完成此操作。该回调然后将包信息放置在自定义数据结构中以使其可用于回放音频队列。

有关回放回调callback的更多信息,请参阅本文档中的Playing Audio,并参阅Audio Queue Services Reference


Using Codecs and Audio Data Formats - 使用编解码器和音频数据格式

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

这里就是如何工作的。 每个音频队列都有一个音频数据格式,以AudioStreamBasicDescription结构表示。 当您在结构体的mFormatID字段中指定格式时,音频队列使用适当的编解码器。 然后指定采样率和通道数,这就是它的全部。 您将看到在Recording AudioPlaying Audio中设置音频数据格式的示例。

录制音频队列使用已安装的编解码器,如图1-5所示。

Figure 1-5 Audio format conversion during recording

在图1-5的第1步中,你的应用程序告诉一个音频队列开始记录,并告诉它要使用的数据格式。 在步骤2中,音频队列获取新的音频数据,并使用编解码器根据您指定的格式进行转换。 音频队列然后调用回调函数,将缓冲区中包含适当格式的音频数据。 在第3步中,您的回调会将格式化的音频数据写入磁盘。 同样,你的回调不需要知道数据格式。

播放音频队列使用已安装的编解码器,如图1-6所示。

Figure 1-6 Audio format conversion during playback

在图1-6的第1步中,你的应用程序通知一个音频队列开始播放,并且还告诉它要播放的音频文件中包含的数据格式。在步骤2中,音频队列调用您的回调,它从音频文件中读取数据。回调将原始格式的数据转交给音频队列。在步骤3中,音频队列使用适当的编解码器,然后将音频发送到目的地。

音频队列可以使用任何已安装的编解码器,无论是原生的Mac OS X还是由第三方提供。要指定要使用的编解码器,请将其四个字符的编码ID提供给音频队列的AudioStreamBasicDescription结构。您将在Recording Audio中看到这个例子。

Mac OS X包含各种各样的音频编码解码器,如CoreAudioTypes.h头文件中的格式ID枚举所示,以及 Core Audio Data Types Reference中所述。您可以使用Audio Toolbox Framework中的AudioFormat.h头文件中的接口来确定系统上可用的编解码器。您可以使用Fiendishthngs应用程序在系统上显示编解码器,可在http://developer.apple.com/samplecode/Fiendishthngs/上以示例代码的形式获得。


Audio Queue Control and State - 音频队列控制和状态

音频队列在创建和销毁之间有一个生命周期。你的应用程序管理这个生命周期,并使用AudioQueue.h头文件中声明的六个函数来控制音频队列的状态:

  • StartAudioQueueStart)。调用开始录制或播放。
  • PrimeAudioQueuePrime)。要进行播放,请在调用AudioQueueStart之前调用,以确保立即有数据可供音频队列播放。此功能与录音无关。
  • StopAudioQueueStop)。调用以重置音频队列(请参阅下面的说明AudioQueueReset),然后停止录制或播放。播放音频队列回调callback在没有更多数据播放时调用此函数。
  • PauseAudioQueuePause)。调用暂停录音或播放而不影响缓冲区或重置音频队列。要恢复,请调用AudioQueueStart函数。
  • FlushAudioQueueFlush)。排队最后一个音频队列缓冲区后调用,以确保所有缓冲的数据以及处理过程中的所有音频数据都被记录或播放。
  • ResetAudioQueueReset)。调用立即静音音频队列,删除以前计划使用的所有缓冲区,并重置所有解码器和DSP状态。

您可以在同步或异步模式下使用AudioQueueStop功能:

  • Synchronous,停止立即发生,不考虑以前缓冲的音频数据。
  • Asynchronous,在所有排队的缓冲区被播放或记录之后发生异步停止。

有关每个功能的完整说明,请参阅Audio Queue Services Reference,其中包括关于音频队列的同步停止和异步停止的更多信息。


Audio Queue Parameters - 音频队列参数

音频队列具有可调参数设置称为parameters。每个参数都有一个枚举常量作为键,一个浮点数作为其值。参数通常用于回放,而不是记录。

在Mac OS X v10.5中,唯一可用的音频队列参数用于增益。此参数的值是使用kAudioQueueParam_Volume常量设置或检索的,其可用范围从0.0到单位增益为1.0。

您的应用程序可以通过两种方式设置音频队列参数

  • 每个音频队列Per audio queue,使用AudioQueueSetParameter函数。这使您可以直接更改音频队列的设置。这些变化立即生效。
  • 每个音频队列缓冲区Per audio queue buffer,使用AudioQueueEnqueueBufferWithParameters函数。这使您可以分配音频队列设置,实际上,您在排入音频队列缓冲区时会将其设置为音频队列缓冲区。音频队列缓冲区开始播放时,这些更改才会生效。

在这两种情况下,音频队列的参数设置都会一直有效,直到您更改它们。

您可以随时使用AudioQueueGetParameter函数访问音频队列的当前参数值。有关获取和设置参数值的函数的完整说明,请参阅Audio Queue Services Reference

后记

未完,待续~~~

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

推荐阅读更多精彩内容