iOS实现播放本地音乐,有非常多种方法。比如AVAudioPlayer,这些都能非常好的胜任。有人就奇怪了。为什么要退而求其次。使用更复杂的AudioQueue来播放本地音乐呢?请继续往下看

AudioQueue简单介绍

AudioQueue,在苹果的开发人员文档上是这么说的

"Audio Queue Services provides a straightforward, low overhead way to record and play audio in iOS and Mac OS X."

AudioQueue官方文档

AudioQueue服务提供一种直接的,低开销的方式以用于在iOS及Mac OS X上录音和播放音乐。

使用AudioQueue播放音乐的长处就是开销非常小并且支持流式播放(边下边播)。可是缺点就是开发难度大。所以有网络音频库AudioStreamer,网上有非常多讲AudioQueue的。可是有实例代码说明的,实在是少之又少。正好公司项目有音频需求,尽管项目中使用的并不是我自己写的音频播放功能,但事后还是想自己来研究一下,这个在我看来比較奇妙也比較有趣的AudioQueue。

AudioStreamer说明

iOS上一个比較有名的流媒体音频播放库是AudioStreamer,该库即使用了AudioQueue。只是该音频库并不支持本地音乐播放,我感觉非常奇怪,为什么作者不支持。并且在使用过程中,我发现该库还是有点问题,尽管我对音频方面的知识并不怎么了解,也并不能与大师媲美并论。但我也希望通过自己的学习,终于完成一个相似AudioStreamer的网络音乐库,眼下或许仅仅是一个设想。无论最后自己有没有那能力,起码我以前也尝试过。只是工作近期比較忙,加上自己知识的欠缺。不知何时才干实现。本次就先来补上AudioStreamer没有支持的,使用AudioQueue播放本地音乐。

AudioQueue具体解释

AudioQueue工作原理

我从Apple的官方文档上截下下面该图:

该图非常好的说明了AudioQueue的工作原理,例如以下说明:

1. 用户调用对应的方法,将音频数据从硬盘中读入到AudioQueue的缓冲区中,并将缓冲区送入音频队列。

2. 用户App通过AudioQueue提供的接口。告诉外放设备,缓冲区中已经有数据。能够拿去播放。

3. 当一个缓冲区中的音频数据播放完成之后,AudioQueue告诉用户,当前有一个空的缓冲区能够用来给你填充数据。

4. 反复以上步骤。直至数据播放完成。

到这里,肯定有不少同学发现了,AudioQueue事实上就是生产者-消费者模型的典型应用。

AudioQueue主要接口

AudioQueueNewOutput

OSStatus AudioQueueNewOutput(const AudioStreamBasicDescription *inFormat, AudioQueueOutputCallback inCallbackProc, void *inUserData, CFRunLoopRef inCallbackRunLoop, CFStringRef inCallbackRunLoopMode, UInt32 inFlags, AudioQueueRef  _Nullable *outAQ);

该方法用于创建一个用于输出音频的AudioQueue

參数及返回说明例如以下:

1. inFormat:该參数指明了即将播放的音频的数据格式

2. inCallbackProc:该回调用于当AudioQueue已使用完一个缓冲区时通知用户,用户能够继续填充音频数据

3. inUserData:由用户传入的数据指针,用于传递给回调函数

4. inCallbackRunLoop:指明回调事件发生在哪个RunLoop之中,假设传递NULL,表示在AudioQueue所在的线程上运行该回调事件,普通情况下,传递NULL就可以。

5. inCallbackRunLoopMode:指明回调事件发生的RunLoop的模式,传递NULL相当于kCFRunLoopCommonModes,通常情况下传递NULL就可以

6. outAQ:该AudioQueue的引用实例,

返回OSStatus,假设值为noErr。则表示没有错误。AudioQueue创建成功。

AudioQueueAllocateBuffer

OSStatus AudioQueueAllocateBuffer(AudioQueueRef inAQ, UInt32 inBufferByteSize, AudioQueueBufferRef  _Nullable *outBuffer);

该方法的作用是为存放音频数据的缓冲区开辟空间

參数及返回说明例如以下:

1. inAQ:AudioQueue的引用实例

2. inBufferByteSize:须要开辟的缓冲区的大小

3. outBuffer:开辟的缓冲区的引用实例

返回OSStatus,假设值为noErr,则表示缓冲区开辟成功。

AudioQueueEnqueueBuffer

OSStatus AudioQueueEnqueueBuffer(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, UInt32 inNumPacketDescs, const AudioStreamPacketDescription *inPacketDescs);

该方法用于将已经填充数据的AudioQueueBuffer入队到AudioQueue

參数及返回说明例如以下:

1. inAQ:AudioQueue的引用实例

2. inBuffer:须要入队的缓冲区实例

3. inNumPacketDescs:缓冲区中共存在有多少帧音频数据

4. inPacketDescs:缓冲区中每一帧的相关信息。用户须要指明当中每一帧在缓冲区中数据的偏移值,通过字段mStartOffset来指定

返回OSStatus。假设值为noErr,则表示缓冲区已经成功入队。等待播放

AudioQueueStart Pause Stop Flush Reset Dispose

OSStatus AudioQueueStart(AudioQueueRef inAQ, const AudioTimeStamp *inStartTime);
OSStatus AudioQueuePause(AudioQueueRef inAQ);
OSStatus AudioQueueStop(AudioQueueRef inAQ, Boolean inImmediate);
OSStatus AudioQueueFlush(AudioQueueRef inAQ);
OSStatus AudioQueueReset(AudioQueueRef inAQ);
OSStatus AudioQueueDispose(AudioQueueRef inAQ, Boolean inImmediate);

顾名思义,前三个方法用于音频的播放,暂停及停止。

后两个方法用于在最后清洗及重置音频队列,清洗确保队列中的数据全然输出。AudioQueuDispose用于清理AudioQueue所占资源。

參数及返回说明例如以下:

1. inAQ:AudioQueue的引用实例

2. inStartTime:指明要開始播放音频的时间,假设要马上開始。传递NULL

3. inImmediate:指明是否要马上停止音频播放,如是。传递true

返回OSStatus表示相关的操作是否成功运行。

AudioQueueFreeBuffer

OSStatus AudioQueueFreeBuffer(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer);

该方法用于在播放结束时。释放清理缓冲区时使用

AudioQueueGetProperty AudioQueueSetProperty

OSStatus AudioQueueGetProperty(AudioQueueRef inAQ, AudioQueuePropertyID inID, void *outData, UInt32 *ioDataSize);
OSStatus AudioQueueSetProperty(AudioQueueRef inAQ, AudioQueuePropertyID inID, const void *inData, UInt32 inDataSize);

此GET/SET方法,用于设置获取AudioQueue的相关属性,请參看AudioQueue.h头文件里的相关说明。

音频播放(LocalAudioPlayer)

使用AudioQueue播放音乐,一般须要配合AudioFileStream一起,AudioFileStream负责解析音频数据。AudioQueue负责播放解析到的音频数据。

此次仅实现最主要的本地音频播放功能。旨在为以后打下基础,不处理不论什么相关的状态(如暂停、停止、SEEK),错误等。

播放方式相似流式播放,仅仅是音频数据来源于本地文件而非网络。需经过下面几个步骤:

1. 持续不断的从文件里读取部分数据,直到数据所有读取结束

2. 将文件里读出的数据,交给AudioFileStream进行数据解析

3. 创建AudioQueue,当数据放入到AudioQueueBuffer中

4. 将缓冲区放到到AudioQueue中,開始播放音频

5. 播放音频结束,清理相关资源

播放器的初始化

播放器的init主要用于指定要播放的音频文件,例如以下所看到的:



读取文件操作,使用NSFileHandle类。audioInUseLock,是一个NSLock*类型,用于在AudioQueue通知我们有空的缓冲区能够使用时做标记。

我们在用户点击playbutton的时候,初始化该播放器,并调用play方法进行播放

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="初始化播放器实例" title="">

播放音频

播放音频将分为下面步骤:

1. 读取并開始解析音频

2. 解析音频信息

3. 解析音频帧

4. 播放音频数据

5. 清理相关资源

我们先定义几个宏,用于指定一些缓冲区的大小

#define kNumberOfBuffers 3              //AudioQueueBuffer数量。一般指明为3
#define kAQBufSize 128 * 1024 //每一个AudioQueueBuffer的大小
#define kAudioFileBufferSize 2048 //文件读取数据的缓冲区大小
#define kMaxPacketDesc 512 //最大的AudioStreamPacketDescription个数

LocalAudioPlayer相关属性

LocalAudioPlayer中定义的属性例如以下所看到的:

读取并開始解析音频

我们使用AudioFileStream来解析音频信息。在用户调用play方法之后。首先调用AudioFileStreamOpen,打开AudioFileStream,例如以下所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="打开AudioFileStream" title="">

extern OSStatus
AudioFileStreamOpen (void *inClientData, AudioFileStream_PropertyListenerProc inPropertyListenerProc, AudioFileStream_PacketsProc inPacketsProc, AudioFileTypeID inFileTypeHint, AudioFileStreamID * outAudioFileStream);

AudioFileStreamOpen的參数说明例如以下:

1. inClientData:用户指定的数据,用于传递给回调函数。这里我们指定(__bridge LocalAudioPlayer*)self

2. inPropertyListenerProc:当解析到一个音频信息时。将回调该方法

3. inPacketsProc:当解析到一个音频帧时,将回调该方法

4. inFileTypeHint:指明音频数据的格式。假设你不知道音频数据的格式,能够传0

5. outAudioFileStream:AudioFileStreamID实例,需保存供兴许使用

读取到数据之后。调用AudioFileStreamParseBytes解析数据,其原型例如以下:

extern OSStatus
AudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream, UInt32 inDataByteSize, const void * inData, AudioFileStreamParseFlags inFlags);

參数的说明例如以下:

1. inAudioFileStream:AudioFileStreamID实例,由AudioFileStreamOpen打开

2. inDataByteSize:此次解析的数据字节大小

3. inData:此次解析的数据大小

4. inFlags:数据解析标志。当中仅仅有一个值kAudioFileStreamParseFlag_Discontinuity = 1,表示解析的数据是否是不连续的,眼下我们能够传0。

当文件数据合部读取结束的时候,此时便能够关闭文件。

解析音频信息

假设解析到音频信息,那么将会调用之前指定的回调函数,例如以下所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="解析音频信息-1" title="">

每一个相关的属性都能够调用AudioFileStreamGetProperty来获取到对应的值,原型例如以下:

extern OSStatus
AudioFileStreamGetProperty(AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 *ioPropertyDataSize, void * outPropertyData);

參数说明:

1. inAudioFileStream:AudioFileStreamID实例,由AudioFileStreamOpen打开

2. inPropertyID:要获取的属性名称。參见AudioFileStream.h

3. ioPropertyDataSize:指明该属性的大小

4. outPropertyData:用于存放该属性值的空间

kAudioFileStreamProperty_DataFormat

该属性指明了音频数据的格式信息,返回的数据是一个AudioStreamBasicDescription结构。需保存用于AudioQueue的使用

kAudioFileStreamProperty_FileFormat

该属性指明了音频数据的编码格式,如MPEG等。

kAudioFileStreamProperty_AudioDataByteCount

该属性可获取到音频数据的长度,可用于计算音频时长,计算公式为:

时长 = (音频数据字节大小 * 8) / 採样率

kAudioFileStreamProperty_BitRate

该属性可获取到音频的採样率。可用于计算音频时长

kAudioFileStreamProperty_DataOffset

该属性指明了音频数据在整个音频文件里的偏移量:

音频文件总大小 = 偏移量 + 音频数据字节大小

kAudioFileStreamProperty_AudioDataPacketCount

该属性指明了音频文件里共同拥有多少帧

kAudioFileStreamProperty_ReadyToProducePackets

该属性告诉我们,已经解析到完整的音频帧数据,准备产生音频帧。之后会调用到另外一个回调函数。我们在这里创建音频队列AudioQueue,假设音频数据中有Magic Cookie Data,则先调用AudioFileStreamGetPropertyInfo,获取该数据是否可写,假设可写再取出该属性值,并写入到AudioQueue。之后便是音频数据帧的解析。

解析音频帧

音频信息解析完成之后,就应该解析音频数据帧了,代码例如以下所看到的:



在这里。我们利用之前设置的inClientData,将该回调函数,由C语言形式改为Objc的形式,解析到音频数据之后的处理代码例如以下所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="解析音频数据帧-2" title="">

解析到音频数据之后,我们要将数据写入到AudioQueueBuffer中,首先,该回调函数的原型例如以下所看到的:

typedef void (*AudioFileStream_PacketsProc)(void *              inClientData, UInt32 inNumberBytes,UInt32       inNumberPackets, const void *inInputData,                           AudioStreamPacketDescription *inPacketDescriptions);

參数说明:

1. inClientData:由AudioFileStreamOpen设置的用户数据

2. inNumberBytes:音频数据的字节数

3. inNumberPackets:解析到的音频帧数

4. inInputData:包括这些音频数据帧的数据

5. inPacketDescriptions:AudioStreamPacketDescription数组。当中包括mStartOffset,指明了该帧相关数据的起始位置。mDataByteSize指明了该帧数据的大小。

此时我们首先创建一个音频队列。用以播放音频。并为每一个缓冲区分配空间,例如以下所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="创建音频队列" title="">

之后我们遍历每一帧。获取每一帧的偏移位置及数据大小,假设当前帧的数据大小,已经无法存放到当前的缓冲区当中,此时,我们应该将当前的缓冲区入队。改动当前使用的缓冲区为下一个。并重置相关的数据填充信息及已包括的数据帧信息。

self.audioQueueCurrentBufferIndex = (++self.audioQueueCurrentBufferIndex) % kNumberOfBuffers;
self.audioPacketsFilled = 0;
self.audioDataBytesFilled = 0;

假设此时还没有開始播放音乐,则能够開始播放音乐

if(self.isPlaying == NO) {
err = AudioQueueStart(audioQueueRef, NULL);
self.isPlaying = YES;
}

若下一个指定的缓冲区已经在使用。即所有缓冲区已满且入队。则应该进行等待

while(inuse[self.audioQueueCurrentBufferIndex]);

最后,假设缓冲区空间还能存放一帧数据,我们就使用memcpy将数据拷贝到缓冲区中对应的位置上去,保存每一帧的相关信息,设置每一帧在缓冲区中的偏移量(mStartOffset),设置当前缓冲区已存方的数据大小。及已包括的帧量。

当一个缓冲区使用结束之后,AudioQueue将会调用之前由AudioQueueNewOutput设置的回调函数,例如以下所看到的:



在该回调中,我们遍历每一个缓冲区,找到空缓冲区。将该缓冲区的标记改动为未使用。

播放音频数据

在处理数据帧的时候。假设缓冲区已经满(指缓冲区的空间不足以存放下一帧数据),则此时能够開始播放音频

if(self.isPlaying == NO) {
err = AudioQueueStart(audioQueueRef, NULL);
self.isPlaying = YES;
}

我们还能够调用AudioQueuePause等相关方法来暂停和终止音频播放。例如以下所看到的:

清理相关资源

终于。我们清理相关资源,在前面,我们利用了AudioQueueAddPropertyListener为kAudioQueueProperty_IsRunning属性设置了一个监听器,当AudioQueue启动或者是终止的时候,会调用该函数:

该回调函数,例如以下所看到的:

在该方法中,我们使用AudioQueueReset重置播放队列,调用AudioQueueFreeBuffer释放缓冲区空间,释放AudioQueue所有资源,关闭AudioFileStream。

只是在实际使用过程中,我发现数据为空的时候AudioQueue并不会主动终止,即不会主动调用该回调。所以我想,应该是要我们自己获取到当前播放的进度,当播放完成的时候调用AudioQueueStop终止播放吧,这个问题留着待以后再继续研究。

结束

终于完成了所有的代码,用户仅仅要点击play,即能够听到《遥远的她》这首美妙的音乐了。因为界面上仅仅有一个playbutton。不放演示图了。戴上耳机。静静的享受自己的成果与这美妙的音乐。

iOS音频播放之AudioQueue(一):播放本地音乐的更多相关文章

  1. iOS音频篇:使用AVPlayer播放网络音乐

    http://www.cocoachina.com/ios/20160324/15767.html 引言 假如你现在打算做一个类似百度音乐.豆瓣电台的在线音乐类APP,你会怎样做? 首先了解一下音频播 ...

  2. iOS音频播放 (五):AudioQueue

    码农人生 ChengYin's coding life 主页 Blog 分类 Categories 归档 Archives 关于 About Weibo GitHub RSS Where there ...

  3. IOS 音频播放

    iOS音频播放 (一):概述 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也因此对于iOS下的音频播放实现有了一定的研究.写这个系列的博客目的一方面希望能够抛砖 ...

  4. iOS音频播放(一):概述

    (本文转自码农人生) 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改,我也因此对于iOS下的音频播放实现有了一定的研究.写这个 系列的博客目的一方面希望能够抛砖引玉 ...

  5. IOS音频1:之采用四种方式播放音频文件(一)AudioToolbox AVFoundation OpenAL AUDIO QUEUE

    本文转载至 http://blog.csdn.net/u014011807/article/details/40187737 在本卷你可以学到什么? 采用四种方法设计应用于各种场合的音频播放器: 基于 ...

  6. iOS音频播放、录音、视频播放、拍照、视频录制

    随着移动互联网的发展,如今的手机早已不是打电话.发短信那么简单了,播放音乐.视频.录音.拍照等都是很常用的功能.在iOS中对于多媒体的支持是非常强大的,无论是音视频播放.录制,还是对麦克风.摄像头的操 ...

  7. iOS音频播放概述

    在iOS系统中apple对音频播放需要的操作进行了封装并提供了不同层次的接口 下面对其中的中高层接口进行功能说明: Audio File Services:读写音频数据,可以完成播放流程中的第2步: ...

  8. iOS音频播放 (二):AudioSession 转

    原文出处 :http://msching.github.io/blog/2014/07/08/audio-in-ios-2/ 前言 本篇为<iOS音频播放>系列的第二篇. 在实施前一篇中所 ...

  9. iOS音频与视频的开发(一)-使用AVAudioPlayer播放音乐、使用AVPlayerViewController播放视频

    iOS的多媒体支持非常强大,它提供了多套支持多媒体的API,无论是音频.视频的播放,还是录制,iOS都提供了多种API支持.借助于这些API的支持,iOS应用既可以查看.播放手机相册中的照片.视频,也 ...

随机推荐

  1. A - Team

    Problem description One day three best friends Petya, Vasya and Tonya decided to form a team and tak ...

  2. 自学Python四 爬虫基础知识储备

    首先,推荐两个关于python爬虫不错的博客:Python爬虫入门教程专栏   和 Python爬虫学习系列教程 .写的都非常不错,我学习到了很多东西!在此,我就我看到的学到的进行总结一下! 爬虫就是 ...

  3. Clustered Index Scan 与 Clustered Index Seek

    Clustered Index Scan 与 Clustered Index Seek 在利用 SQL Server 查询分析器的执行计划中,会有许多扫描方式,其中就有 Clustered Index ...

  4. Deutsch lernen (04)

    1. streng a. 严厉的,严格的 streng gegen sich selbst und gegen andere sein streng auf Ordnung halten 2. ver ...

  5. LINQ to Entities 不识别方法“System.Nullable`1[System.Int32] DiffDays(System.Nullable`1[System.DateTime], System.Nullable`1[System.DateTime])”,因此该方法无法转换为存储表达式。

    解决方案: db.table.Where(m=>System.Data.Objects.EntityFunctions.DiffDays(m.CreateTime, DateTime.Now) ...

  6. js 验证文件格式和大小

    <script> $('#btnSearch').click(function(){ // alert("000");// fileElem = document.ge ...

  7. Python统计字符串中出现次数最多的人名

    人名最多数统计题目摘自https://python123.io 描述编程模板中给出了一个字符串,其中包含了含有重复的人名,请直接输出出现最多的人名.‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬ ...

  8. PAT_A1034#Head of a Gang

    Source: PAT A1034 Head of a Gang (30 分) Description: One way that the police finds the head of a gan ...

  9. 终于等到你!微软正式上线 Windows Terminal 预览版

    前一段时间,一直在知乎.技术社区收到技术小伙伴们的终极拷问:微软Build 大会上提到的**6月中旬**要上Windows store 的 Windows Terminal 到底啥时候可以用到呀? 有 ...

  10. 无法启用internet连接共享,为LAN连接配置的IP地址需要使用自动IP寻址

    热点不能用了,一直都不知道为什么,今天查了一些资料,终于知道了原因,是因为我安装了VMware Workstation Pro ,它生成了VMnet1和VMnet8所在的两个网段,这个网段就和热点共享 ...