几种播放音频文件的方式(八) —— 音频队列服务(Audio Queue Services)之播放音频(五)

版本记录

版本号 时间
V1.0 2017.12.28

前言

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

Playing Audio - 播放音频

使用音频队列服务播放音频时,音源可以是任何东西 - 磁盘上文件,基于软件的音频合成器,内存中的对象等等。本章介绍最常见的情况:播放磁盘上的文件。

注意:本章介绍了一个基于ANSI-C的播放实现,并且使用了Mac OS X Core Audio SDK中的C ++类。对于基于Objective-C的示例,请参阅iOS Dev CenterSpeakHere示例代码。

要为您的应用程序添加播放功能,通常需要执行以下步骤:

    1. 定义一个自定义结构来管理状态,格式和路径信息。
    1. 编写音频队列回调函数来执行实际播放。
    1. 编写代码来确定音频队列缓冲区的大小。
    1. 打开音频文件进行播放并确定其音频数据格式。
    1. 创建一个播放音频队列并将其配置为播放。
    1. 分配和入队音频队列缓冲区。告诉音频队列开始播放。完成后,回放回调告知音频队列停止。
    1. 销毁音频队列,释放资源。

本章的其余部分将详细介绍这些步骤。


Define a Custom Structure to Manage State - 定义一个自定义结构管理状态

首先,定义一个用来管理音频格式和音频队列状态信息的自定义结构。 Listing 3-1说明了这样一个结构:

// Listing 3-1  A custom structure for a playback audio queue

static const int kNumberBuffers = 3;                              // 1
struct AQPlayerState {
    AudioStreamBasicDescription   mDataFormat;                    // 2
    AudioQueueRef                 mQueue;                         // 3
    AudioQueueBufferRef           mBuffers[kNumberBuffers];       // 4
    AudioFileID                   mAudioFile;                     // 5
    UInt32                        bufferByteSize;                 // 6
    SInt64                        mCurrentPacket;                 // 7
    UInt32                        mNumPacketsToRead;              // 8
    AudioStreamPacketDescription  *mPacketDescs;                  // 9
    bool                          mIsRunning;                     // 10
};

此结构中的大多数字段与用于记录的自定义结构中的字段相同(或接近),如Define a Custom Structure to Manage State中的“录制音频”章节中所述。例如,mDataFormat字段在此用于保存正在播放的文件的格式。录制时,类似字段保存正在写入磁盘的文件的格式。

以下是这个结构中的字段的描述:

    1. 设置要使用的音频队列缓冲区的数量。如Audio Queue Buffers中所述,“三”通常是一个很好的数字。
    1. 表示正在播放的文件的音频数据格式的AudioStreamBasicDescription结构(来自CoreAudioTypes.h)。该格式被mQueue字段中指定的音频队列使用。通过查询音频文件的kAudioFilePropertyDataFormat属性来填充mDataFormat字段,如Obtaining a File’s Audio Data Format中所述。有关AudioStreamBasicDescription结构的详细信息,请参阅Core Audio Data Types Reference
    1. 由应用程序创建的播放音频队列。
    1. 一个数组,指向由音频队列管理的音频队列缓冲区的指针。
    1. 音频文件对象,表示您的程序播放的音频文件。
    1. 每个音频队列缓冲区的大小(以字节为单位)。在创建音频队列之后并在启动之前,在DeriveBufferSize函数的这些示例中计算此值。请参阅 Write a Function to Derive Playback Audio Queue Buffer Size
    1. 从音频文件播放下一个数据包的数据包索引。
    1. 每次调用音频队列的回放回调时读取的数据包数量。与bufferByteSize字段类似,在创建音频队列之后并在启动之前,在DeriveBufferSize函数的这些示例中计算此值。
    1. 对于VBR音频数据,正在播放的文件的数据包描述数组。对于CBR数据,该字段的值为NULL。
    1. 指示音频队列是否正在运行的布尔值。

Write a Playback Audio Queue Callback - 编写播放音频队列回调

接下来,写一个回放音频队列回调函数。 这个回调有三个主要的事情:

  • 从音频文件读取指定数量的数据并将其放入音频队列缓冲区
  • 将音频队列缓冲区排入缓冲队列
  • 当没有更多的数据要从音频文件读取,告诉音频队列停止

本部分显示了一个示例回调声明,分别描述了这些任务,并最终呈现整个回放回调。 有关回放回调角色的说明,请参阅图Figure 1-4

1. The Playback Audio Queue Callback Declaration - 播放音频队列回调函数声明

Listing 3-2显示了一个播放音频队列回调函数的示例声明,声明为AudioQueue.h头文件中的AudioQueueOutputCallback

// Listing 3-2  The playback audio queue callback declaration

static void HandleOutputBuffer (
    void                 *aqData,                 // 1
    AudioQueueRef        inAQ,                    // 2
    AudioQueueBufferRef  inBuffer                 // 3
)

以下是这段代码的工作原理:

    1. 通常,aqData是包含音频队列状态信息的自定义结构,如Define a Custom Structure to Manage State所述。
    1. 拥有此回调的音频队列。
    1. 一个音频队列缓冲区,回调将通过读取音频文件来填充数据。

2. Reading From a File into an Audio Queue Buffer - 从文件中读取数据到音频队列缓冲区

回放音频队列回调的第一个动作是从音频文件读取数据并将其放入音频队列缓冲区。Listing 3-3显示了如何做到这一点。

// Listing 3-3  Reading from an audio file into an audio queue buffer

AudioFileReadPackets (                        // 1
    pAqData->mAudioFile,                      // 2
    false,                                    // 3
    &numBytesReadFromFile,                    // 4
    pAqData->mPacketDescs,                    // 5
    pAqData->mCurrentPacket,                  // 6
    &numPackets,                              // 7
    inBuffer->mAudioData                      // 8
);

以下是这段代码的工作原理:

    1. AudioFile.h头文件中声明的AudioFileReadPackets函数从音频文件读取数据并将其放入缓冲区。
    1. 要从中读取的音频文件。
    1. 使用值为false来指示函数在读取时不应该缓存数据。
    1. 输出时,从音频文件中读取的音频数据的字节数。
    1. 在输出上,从音频文件中读取数据的数据包描述数组。 对于CBR数据,此参数的输入值为NULL。
    1. 从音频文件中读取的第一个数据包数据包索引。
    1. 输入时,从音频文件中读取的数据包数量。 输出时,实际读取的数据包数量。
    1. 输出时,填充的音频队列缓冲区包含从音频文件中读取的数据。

3. Enqueuing an Audio Queue Buffer - 声频队列缓冲入队

现在,数据已经从音频文件中读取并放入音频队列缓冲区,回调将缓冲区排入队列中,如Listing 3-4所示。 一旦进入缓冲区队列,缓冲区中的音频数据就可供音频队列发送到输出设备。

// Listing 3-4  Enqueuing an audio queue buffer after reading from disk

AudioQueueEnqueueBuffer (                      // 1
    pAqData->mQueue,                           // 2
    inBuffer,                                  // 3
    (pAqData->mPacketDescs ? numPackets : 0),  // 4
    pAqData->mPacketDescs                      // 5
);

以下是这段代码的工作原理:

    1. AudioQueueEnqueueBuffer函数将音频队列缓冲区添加到缓冲区队列中。
    1. 拥有缓冲队列buffer queue的音频队列audio queue
    1. 要排队的音频队列缓冲区
    1. 音频队列缓冲区数据中表示的数据包数量。 对于不使用数据包描述的CBR数据,使用0。
    1. 对于使用数据包描述的压缩音频数据格式,缓冲区中数据包的数据包描述。

4. Stopping an Audio Queue - 停止音频队列

你回调的最后一件事是检查是否没有更多的数据从你正在播放的音频文件中读取。 一旦发现文件结束,你的回调告诉播放音频队列停止。 Listing 3-5说明了这一点。

// Listing 3-5  Stopping an audio queue

if (numPackets == 0) {                          // 1
    AudioQueueStop (                            // 2
        pAqData->mQueue,                        // 3
        false                                   // 4
    );
    pAqData->mIsRunning = false;                // 5
}

以下是这段代码的工作原理:

    1. 检查AudioFileReadPackets函数读取的数据包的数量是否为0。
    1. AudioQueueStop函数停止音频队列。
    1. 要停止的音频队列。
    1. 当所有排队的缓冲区都被播放时,异步停止音频队列。 请参阅Audio Queue Control and State
    1. 在自定义结构中设置一个标志来指示播放完成。

5. A Full Playback Audio Queue Callback - 播放音频队列回调函数完整版

Listing 3-6显示了完整播放音频队列回调的基本版本。 与本文档中的其他代码示例一样,此列表不包括错误处理。

// Listing 3-6  A playback audio queue callback function

static void HandleOutputBuffer (
    void                *aqData,
    AudioQueueRef       inAQ,
    AudioQueueBufferRef inBuffer
) {
    AQPlayerState *pAqData = (AQPlayerState *) aqData;        // 1
    if (pAqData->mIsRunning == 0) return;                     // 2
    UInt32 numBytesReadFromFile;                              // 3
    UInt32 numPackets = pAqData->mNumPacketsToRead;           // 4
    AudioFileReadPackets (
        pAqData->mAudioFile,
        false,
        &numBytesReadFromFile,
        pAqData->mPacketDescs, 
        pAqData->mCurrentPacket,
        &numPackets,
        inBuffer->mAudioData 
    );
    if (numPackets > 0) {                                     // 5
        inBuffer->mAudioDataByteSize = numBytesReadFromFile;  // 6
       AudioQueueEnqueueBuffer ( 
            pAqData->mQueue,
            inBuffer,
            (pAqData->mPacketDescs ? numPackets : 0),
            pAqData->mPacketDescs
        );
        pAqData->mCurrentPacket += numPackets;                // 7 
    } else {
        AudioQueueStop (
            pAqData->mQueue,
            false
        );
        pAqData->mIsRunning = false; 
    }
}

以下是这段代码的工作原理:

    1. 实例化时提供给音频队列的自定义数据,包括表示要播放的文件的音频文件对象(类型AudioFileID)以及各种状态数据。 请参阅 Define a Custom Structure to Manage State
    1. 如果音频队列停止,立即返回。
    1. 保存正在播放的文件中读取的音频数据字节数的变量。
    1. 使用要从正在播放的文件中读取的数据包数初始化numPackets变量。
    1. 测试是否从文件中检索到某些音频数据。 如果是,排队新填充的缓冲区。 如果不是,则停止音频队列。
    1. 告诉音频队列缓冲区结构读取数据的字节数。
    1. 根据读取的数据包数量递增数据包索引。

Write a Function to Derive Playback Audio Queue Buffer Size - 编写函数获取播放音频队列缓冲区的大小

音频队列服务期望您的应用程序指定您使用的音频队列缓冲区的大小。 Listing 3-7显示了一种方法。 它产生足够大的缓冲区大小来保存给定持续时间的音频数据。

在创建一个回放音频队列之后,您将在您的应用程序中调用此DeriveBufferSize函数,作为要求音频队列分配缓冲区的先决条件。 请参阅Set Sizes for a Playback Audio Queue

与你在Write a Function to Derive Recording Audio Queue Buffer Size类似的函数相比,这里的代码做了两个额外的事情, 对于回放同样如此:

    1. 每次您的回调调用AudioFileReadPackets函数时,要读取数据包的数量
    1. 设置缓冲区大小的下限,以避免过度频繁的磁盘访问

这里的计算考虑了您从磁盘读取的音频数据格式。 格式包括可能影响缓冲区大小的所有因素,例如音频通道的数量。

// Listing 3-7  Deriving a playback audio queue buffer size

void DeriveBufferSize (
    AudioStreamBasicDescription &ASBDesc,                            // 1
    UInt32                      maxPacketSize,                       // 2
    Float64                     seconds,                             // 3
    UInt32                      *outBufferSize,                      // 4
    UInt32                      *outNumPacketsToRead                 // 5
) {
    static const int maxBufferSize = 0x50000;                        // 6
    static const int minBufferSize = 0x4000;                         // 7
 
    if (ASBDesc.mFramesPerPacket != 0) {                             // 8
        Float64 numPacketsForTime =
            ASBDesc.mSampleRate / ASBDesc.mFramesPerPacket * seconds;
        *outBufferSize = numPacketsForTime * maxPacketSize;
    } else {                                                         // 9
        *outBufferSize =
            maxBufferSize > maxPacketSize ?
                maxBufferSize : maxPacketSize;
    }
 
    if (                                                             // 10
        *outBufferSize > maxBufferSize &&
        *outBufferSize > maxPacketSize
    )
        *outBufferSize = maxBufferSize;
    else {                                                           // 11
        if (*outBufferSize < minBufferSize)
            *outBufferSize = minBufferSize;
    }
 
    *outNumPacketsToRead = *outBufferSize / maxPacketSize;           // 12
}

以下是这段代码的工作原理:

    1. 音频队列的AudioStreamBasicDescription结构。
    1. 您正在播放的音频文件中数据的估计最大数据包大小。您可以通过调用AudioFileGetProperty函数(在AudioFile.h头文件中声明)使用属性ID kAudioFilePropertyPacketSizeUpperBound来确定此值。请参阅Set Sizes for a Playback Audio Queue
    1. 按照音频的秒数指定每个音频队列缓冲区的大小。
    1. 输出时,每个音频队列缓冲区的大小(以字节为单位)。
    1. 输出时,每次调用回放音频队列回调时从文件中读取的音频数据包的数量。
    1. 音频队列缓冲区大小的上限(以字节为单位)。在这个例子中,上限设置为320 KB。这对应于大约五秒钟的立体声,采样率为96kHz的24位音频。
    1. 音频队列缓冲区大小的下限,以字节为单位。在这个例子中,下限设置为16 KB。
    1. 对于定义每个数据包固定数量帧的音频数据格式,获取音频队列缓冲区大小。
    1. 对于没有为每个数据包定义固定数量帧的音频数据格式,根据最大数据包大小和您设置的上限获取合理的音频队列缓冲区大小。
    1. 如果导出的缓冲区大小高于您设置的上限,则根据估计的最大数据包大小调整边界考虑。
    1. 如果导出的缓冲区大小低于您设置的下限,则将其调整到界限。
    1. 计算每次调用回调时从音频文件中读取的数据包数量。

Open an Audio File for Playback - 打开用于播放的音频文件

现在您打开一个音频文件进行播放,使用以下三个步骤:

    1. 获取表示要播放的音频文件的CFURL对象。
    1. 打开文件。
    1. 获取文件的音频数据格式

1. Obtaining a CFURL Object for an Audio File - 从音频文件中获取CFURL对象

Listing 3-8演示了如何获取要播放的音频文件的CFURL对象。 在下一步中使用CFURL对象,打开文件。

// Listing 3-8  Obtaining a CFURL object for an audio file

CFURLRef audioFileURL =
    CFURLCreateFromFileSystemRepresentation (           // 1
        NULL,                                           // 2
        (const UInt8 *) filePath,                       // 3
        strlen (filePath),                              // 4
        false                                           // 5
    );

以下是这段代码的工作原理:

    1. CFURL.h头文件中声明的CFURLCreateFromFileSystemRepresentation函数创建一个代表要播放文件的CFURL对象。
    1. 使用NULL(或kCFAllocatorDefault)来使用当前的默认内存分配器。
    1. 您要转换为CFURL对象的文件系统路径。 在生产代码中,通常会从用户获取filePath的值。
    1. 文件系统路径中的字节数。
    1. 值为false表示filePath表示文件,而不是文件夹directory

2. Opening an Audio File - 打开音频文件

Listing 3-9演示了如何打开一个音频文件进行播放。

// Listing 3-9  Opening an audio file for playback

AQPlayerState aqData;                                   // 1
 
OSStatus result =
    AudioFileOpenURL (                                  // 2
        audioFileURL,                                   // 3
        fsRdPerm,                                       // 4
        0,                                              // 5
        &aqData.mAudioFile                              // 6
    );
 
CFRelease (audioFileURL);                               // 7

以下是这段代码的工作原理:

    1. 创建AQPlayerState自定义结构的实例(请参阅Define a Custom Structure to Manage State)。 当您打开音频文件进行播放时,您可以使用此实例作为放置表示音频文件的音频文件对象(AudioFileID类型)的位置。
    1. AudioFile.h头文件中声明的AudioFileOpenURL函数打开你想要播放的文件。
    1. 对要播放的文件的引用。
    1. 您想要与您正在播放的文件一起使用的文件权限。 可用权限在文件管理器的File Access Permission Constants枚举中定义。 在这个例子中你要求读取文件的权限。
    1. 一个可选的文件类型提示。 此处的值为0表示该示例不使用此功能。
    1. 在输出时,对音频文件的引用被放置在自定义结构的mAudioFile字段中。
    1. 释放在步骤1中创建的CFURL对象。

3. Obtaining a File’s Audio Data Format - 获取文件音频数据格式

Listing 3-10显示了如何获取文件的音频数据格式

Listing 3-10  Obtaining a file’s audio data format

UInt32 dataFormatSize = sizeof (aqData.mDataFormat);    // 1
 
AudioFileGetProperty (                                  // 2
    aqData.mAudioFile,                                  // 3
    kAudioFilePropertyDataFormat,                       // 4
    &dataFormatSize,                                    // 5
    &aqData.mDataFormat                                 // 6
);

以下是这段代码的工作原理:

    1. 获取预期的属性值大小,用于查询音频文件的音频数据格式。
    1. AudioFile.h头文件中声明的AudioFileGetProperty函数获取音频文件中指定属性的值。
    1. 音频文件对象(类型为AudioFileID),代表要获取其音频数据格式的文件。
    1. 用于获取音频文件的数据格式的值的属性ID。
    1. 输入时,描述音频文件数据格式的AudioStreamBasicDescription结构的预期大小。 在输出上,实际的大小。 您的回放应用程序不需要使用此值。
    1. 在输出时,以音频文件的形式从AudioStreamBasicDescription结构中获得完整的音频数据格式。 该行将文件的音频数据格式存储到音频队列的自定义结构中,以将其应用于音频队列。

Create a Playback Audio Queue - 创建播放音频队列

Listing 3-11显示了如何创建一个回放音频队列。 请注意,AudioQueueNewOutput函数使用前面步骤中配置的自定义结构和回调,以及要播放的文件的音频数据格式。

// Listing 3-11  Creating a playback audio queue

AudioQueueNewOutput (                                // 1
    &aqData.mDataFormat,                             // 2
    HandleOutputBuffer,                              // 3
    &aqData,                                         // 4
    CFRunLoopGetCurrent (),                          // 5
    kCFRunLoopCommonModes,                           // 6
    0,                                               // 7
    &aqData.mQueue                                   // 8
);

以下是这段代码的工作原理:

    1. AudioQueueNewOutput函数创建一个新的播放音频队列。
    1. 音频队列正在设置播放的文件的音频数据格式。 请参阅 Obtaining a File’s Audio Data Format
    1. 与回放音频队列一起使用的回调函数。 请参阅Write a Playback Audio Queue Callback
    1. 播放音频队列的自定义数据结构。 请参阅Define a Custom Structure to Manage State
    1. 当前运行循环,以及将调用音频队列回放回调的循环。
    1. 可以调用回调的运行循环模式。 通常,在这里使用kCFRunLoopCommonModes常量。
    1. 保留。 必须为0。
    1. 输出时,新分配的播放音频队列。

Set Sizes for a Playback Audio Queue - 设置回放音频队列的大小

接下来,您为播放音频队列设置一些大小。 在为音频队列分配缓冲区之前以及在开始读取音频文件之前,请使用这些大小。

本节中的代码清单显示如何设置:

  • 音频队列缓冲区大小
  • 每次调用回放音频队列回调时要读取的数据包数量
  • 数组大小,用于保存一个缓冲区的音频数据的数据包描述

1. Setting Buffer Size and Number of Packets to Read - 设置缓存大小和要读取的包的数量

Listing 3-12演示了如何使用之前编写的DeriveBufferSize函数(请参阅 Write a Function to Derive Playback Audio Queue Buffer Size)。 此处的目标是为每个音频队列缓冲区设置一个以字节为单位的大小,并确定每次调用回放音频队列回调时要读取的数据包数量。

此代码使用保守估计的最大数据包大小,Core Audio通过kAudioFilePropertyPacketSizeUpperBound属性提供。 在大多数情况下,与比花费时间读取整个音频文件来获得实际的最大数据包大小相比,最好使用这种技术 - 这是近似而快速的 。

// Listing 3-12  Setting playback audio queue buffer size and number of packets to read

UInt32 maxPacketSize;
UInt32 propertySize = sizeof (maxPacketSize);
AudioFileGetProperty (                               // 1
    aqData.mAudioFile,                               // 2
    kAudioFilePropertyPacketSizeUpperBound,          // 3
    &propertySize,                                   // 4
    &maxPacketSize                                   // 5
);
 
DeriveBufferSize (                                   // 6
    aqData.mDataFormat,                              // 7
    maxPacketSize,                                   // 8
    0.5,                                             // 9
    &aqData.bufferByteSize,                          // 10
    &aqData.mNumPacketsToRead                        // 11
);

以下是这段代码的工作原理:

    1. AudioFile.h头文件中声明的AudioFileGetProperty函数获取音频文件的指定属性的值。在这里,你用它来获得一个保守的上限,以字节为单位,你想播放的文件中的音频数据包的大小。
    1. 表示要播放的文件的音频文件对象(类型为AudioFileID)。请参阅打Opening an Audio File
    1. 用于获取音频文件中数据包大小保守上限的属性ID。
    1. 在输出上,kAudioFilePropertyPacketSizeUpperBound属性的大小(以字节为单位)。
    1. 在输出上,对于要播放的文件,数据包大小的保守上限(以字节为单位)。
    1. Write a Function to Derive Playback Audio Queue Buffer Size中描述的DeriveBufferSize函数设置每次调用回放音频队列回调时要读取的缓冲区大小和数据包的数量。
    1. 您要播放的文件的音频数据格式。请参阅Obtaining a File’s Audio Data Format
    1. 音频文件中估计的最大数据包大小,来自此列表的第5行。
    1. 每个音频队列缓冲区应该容纳的音频的秒数。半秒钟,如这里设置,通常是一个不错的选择。
    1. 输出时,每个音频队列缓冲区的大小(以字节为单位)。该值放置在音频队列的自定义结构中。
    1. 输出时,每次调用播放音频队列回调时要读取的数据包数量。该值也放置在音频队列的自定义结构中。

2. Allocating Memory for a Packet Descriptions Array - 为包的描述数组分配内存

现在,您为数组分配内存,以保存一个缓冲区的音频数据的数据包描述。 恒定比特率数据不使用数据包描述,因此Listing 3-13中的CBR情况步骤3非常简单。

// Listing 3-13  Allocating memory for a packet descriptions array

bool isFormatVBR = (                                       // 1
    aqData.mDataFormat.mBytesPerPacket == 0 ||
    aqData.mDataFormat.mFramesPerPacket == 0
);
 
if (isFormatVBR) {                                         // 2
    aqData.mPacketDescs =
      (AudioStreamPacketDescription*) malloc (
        aqData.mNumPacketsToRead * sizeof (AudioStreamPacketDescription)
      );
} else {                                                   // 3
    aqData.mPacketDescs = NULL;
}

以下是这段代码的工作原理:

    1. 确定音频文件的数据格式是VBR还是CBR。 在VBR数据中,每个字节数据包或帧每个数据包值中的一个或两个是可变的,因此在音频队列的AudioStreamBasicDescription结构中将被列为0。
    1. 对于包含VBR数据的音频文件,为数据包描述数组分配内存。 根据每次调用回放回调时要读取的音频数据包的数量来计算所需的内存。 请参阅Setting Buffer Size and Number of Packets to Read
    1. 对于包含CBR数据(如线性PCM)的音频文件,音频队列不使用数据包描述数组。

Set a Magic Cookie for a Playback Audio Queue - 为播放音频队列设置Magic Cookie

某些压缩音频格式(如MPEG 4 AAC)利用结构来包含音频元数据。 这些结构被称为Magic Cookie。 当您使用音频队列服务以这种格式播放文件时,您将从音频文件中获取Magic Cookie,并在开始播放之前将其添加到音频队列中。

Listing 3-14展示了如何从一个文件中获得一个Magic Cookie并将其应用到一个音频队列中。 开始播放之前,您的代码会调用此函数。

// Listing 3-14  Setting a magic cookie for a playback audio queue

UInt32 cookieSize = sizeof (UInt32);                   // 1
bool couldNotGetProperty =                             // 2
    AudioFileGetPropertyInfo (                         // 3
        aqData.mAudioFile,                             // 4
        kAudioFilePropertyMagicCookieData,             // 5
        &cookieSize,                                   // 6
        NULL                                           // 7
    );
 
if (!couldNotGetProperty && cookieSize) {              // 8
    char* magicCookie =
        (char *) malloc (cookieSize);
 
    AudioFileGetProperty (                             // 9
        aqData.mAudioFile,                             // 10
        kAudioFilePropertyMagicCookieData,             // 11
        &cookieSize,                                   // 12
        magicCookie                                    // 13
    );
 
    AudioQueueSetProperty (                            // 14
        aqData.mQueue,                                 // 15
        kAudioQueueProperty_MagicCookie,               // 16
        magicCookie,                                   // 17
        cookieSize                                     // 18
    );
 
    free (magicCookie);                                // 19
}

以下是这段代码的工作原理:

    1. 为magic cookie数据设置估计大小。
    1. 捕获AudioFileGetPropertyInfo函数的结果。如果成功,则此函数返回NoErr的值,相当于布尔值false。
    1. AudioFile.h头文件中声明的AudioFileGetPropertyInfo函数获取指定属性值的大小。您可以使用它来设置保存属性值的变量的大小。
    1. 音频文件对象(类型为AudioFileID),表示要播放的音频文件。
    1. 属性ID代表音频文件的magic cookie数据。
    1. 在输入时,magic cookie数据的估计大小。在输出上,实际的大小。
    1. 使用NULL来表示您不关心属性的读/写访问权限。
    1. 如果音频文件包含一个magic cookie,分配内存来持有它。
    1. AudioFile.h头文件中声明的AudioFileGetProperty函数获取指定属性的值。在这种情况下,它会获取音频文件的magic cookie。
    1. 音频文件对象(类型为AudioFileID),表示您要播放的音频文件,以及您获取的magic cookie。
    1. 表示音频文件magic cookie数据的属性ID。
    1. 在输入上,使用AudioFileGetPropertyInfo函数获得的magicCookie变量的大小。在输出上,根据写入到magicCookie变量的字节数来计算magic cookie的实际大小。
    1. 输出时,音频文件的magic cookie。
    1. AudioQueueSetProperty函数在音频队列中设置一个属性。在这种情况下,它为音频队列设置一个magic cookie,匹配要播放的音频文件中的magic cookie。
    1. 您要为其设置magic cookie的音频队列。
    1. 属性ID代表音频队列的magic cookie。
    1. 您要播放的音频文件中的magic cookie。
    1. magic cookie的大小,以字节为单位。
    1. 释放为magic cookie分配的内存。

Allocate and Prime Audio Queue Buffers - 分配和填充音频队列缓冲区

现在您可以询问您创建的音频队列(在Create a Playback Audio Queue中)以准备一组音频队列缓冲区。 Listing 3-15演示了如何做到这一点。

// Listing 3-15  Allocating and priming audio queue buffers for playback

aqData.mCurrentPacket = 0;                                // 1
 
for (int i = 0; i < kNumberBuffers; ++i) {                // 2
    AudioQueueAllocateBuffer (                            // 3
        aqData.mQueue,                                    // 4
        aqData.bufferByteSize,                            // 5
        &aqData.mBuffers[i]                               // 6
    );
 
    HandleOutputBuffer (                                  // 7
        &aqData,                                          // 8
        aqData.mQueue,                                    // 9
        aqData.mBuffers[i]                                // 10
    );
}

以下是这段代码的工作原理:

    1. 将数据包索引设置为0,以便当音频队列回调开始填充缓冲区时(步骤7),它将从音频文件的开始处开始。
    1. 分配和填充一组音频队列缓冲区。 (您可以在Define a Custom Structure to Manage State。将此编号kNumberBuffers设置为3)。
    1. AudioQueueAllocateBuffer函数通过为其分配内存来创建音频队列缓冲区。
    1. 正在分配音频队列缓冲区的音频队列。
    1. 新的音频队列缓冲区的大小(以字节为单位)。
    1. 在输出上,将新的音频队列缓冲区添加到自定义结构中的mBuffers数组中。
    1. HandleOutputBuffer函数是你写的回放音频队列回调函数。 请参阅 Write a Playback Audio Queue Callback
    1. 音频队列的自定义结构。
    1. 您正在调用的回调的音频队列。
    1. 您传递给音频队列回调的音频队列缓冲区。

Set an Audio Queue’s Playback Gain - 设置音频队列的播放增益

在您告诉音频队列开始播放之前,您需要通过音频队列参数机制设置其增益。 Listing 3-16显示了如何做到这一点。 有关参数机制的更多信息,请参阅Audio Queue Parameters

// Listing 3-16  Setting an audio queue’s playback gain

Float32 gain = 1.0;                                       // 1
    // Optionally, allow user to override gain setting here
AudioQueueSetParameter (                                  // 2
    aqData.mQueue,                                        // 3
    kAudioQueueParam_Volume,                              // 4
    gain                                                  // 5
);

以下是这段代码的工作原理:

    1. 在0(用于静音)和1(用于单位增益)之间设置与音频队列一起使用的增益。
    1. AudioQueueSetParameter函数设置音频队列的参数值。
    1. 您正在设置参数的音频队列。
    1. 您正在设置的参数的ID。 kAudioQueueParam_Volume常量让您设置音频队列的增益。
    1. 您正在应用到音频队列的增益设置。

Start and Run an Audio Queue - 开始和运行音频队列

所有前面的代码都说明了播放文件的过程。 这包括启动音频队列并在文件播放时保持运行循环,如Listing 3-17所示

// Listing 3-17  Starting and running an audio queue

aqData.mIsRunning = true;                          // 1
 
AudioQueueStart (                                  // 2
    aqData.mQueue,                                 // 3
    NULL                                           // 4
);
 
do {                                               // 5
    CFRunLoopRunInMode (                           // 6
        kCFRunLoopDefaultMode,                     // 7
        0.25,                                      // 8
        false                                      // 9
    );
} while (aqData.mIsRunning);
 
CFRunLoopRunInMode (                               // 10
    kCFRunLoopDefaultMode,
    1,
    false
);

以下是这段代码的工作原理:

    1. 在自定义结构中设置一个标志来指示音频队列正在运行。
    1. AudioQueueStart函数在其自己的线程上启动音频队列。
    1. 音频队列开始。
    1. 使用NULL来指示音频队列应该立即开始播放。
    1. 定期轮询自定义结构的mIsRunning字段,以检查音频队列是否已停止。
    1. CFRunLoopRunInMode函数运行包含音频队列线程的运行循环。
    1. 使用运行循环的默认模式。
    1. 将运行循环的运行时间设置为0.25秒。
    1. 使用false来指示运行循环应该持续指定的全部时间。
    1. 音频队列停止后,再运行一次运行循环,以确保当前正在播放的音频队列缓冲区有时间完成。

Clean Up After Playing - 播放完后的清理

当您完成播放文件时,请释放音频队列,关闭音频文件并释放剩余的资源。 Listing 3-18说明了这些步骤。

// Listing 3-18  Cleaning up after playing an audio file

AudioQueueDispose (                            // 1
    aqData.mQueue,                             // 2
    true                                       // 3
);
 
AudioFileClose (aqData.mAudioFile);            // 4
 
free (aqData.mPacketDescs);                    // 5

以下是这段代码的工作原理:

    1. AudioQueueDispose函数销毁音频队列及其所有资源,包括其缓冲区。
    1. 您想要处理的音频队列。
    1. 使用true来同步销毁音频队列。
    1. 关闭播放的音频文件。 AudioFileClose函数在AudioFile.h头文件中声明。
    1. 释放用来保存数据包描述的内存。

后记

未完,待续~~~

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

推荐阅读更多精彩内容