• AudioQueue简介
  • AudioStreamer说明
  • AudioQueue详解
    • AudioQueue工作原理
    • AudioQueue主要接口
      • AudioQueueNewOutput
      • AudioQueueAllocateBuffer
      • AudioQueueEnqueueBuffer
      • AudioQueueStart Pause Stop Flush Reset Dispose
      • AudioQueueFreeBuffer
      • AudioQueueGetProperty AudioQueueSetProperty
  • 音频播放LocalAudioPlayer
    • 播放器的初始化
    • 播放音频
      • LocalAudioPlayer相关属性
      • 读取并开始解析音频
      • 解析音频信息
        • kAudioFileStreamProperty_DataFormat
        • kAudioFileStreamProperty_FileFormat
        • kAudioFileStreamProperty_AudioDataByteCount
        • kAudioFileStreamProperty_BitRate
        • kAudioFileStreamProperty_DataOffset
        • kAudioFileStreamProperty_AudioDataPacketCount
        • kAudioFileStreamProperty_ReadyToProducePackets
      • 解析音频帧
      • 播放音频数据
      • 清理相关资源
  • 结束

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通知我们有空的缓冲区可以使用时做标记。

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

播放音频

播放音频将分为以下步骤:
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,如下所示:

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。

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

解析音频信息

如果解析到音频信息,那么将会调用之前指定的回调函数,如下所示:

每个相关的属性都可以调用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的形式,解析到音频数据之后的处理代码如下所示:

解析到音频数据之后,我们要将数据写入到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指明了该帧数据的大小。

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

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

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,即可以听到《遥远的她》这首美妙的音乐了。由于界面上只有一个play按钮,不放演示图了,戴上耳机,静静的享受自己的成果与这美妙的音乐。。

iOS音频播放之AudioQueue(一):播放本地音乐相关推荐

  1. ios html背景音乐,iOS音频篇:使用AVPlayer播放网络音乐

    作者:明仔Su(简书) 引言 假如你现在打算做一个类似百度音乐.豆瓣电台的在线音乐类APP,你会怎样做? 首先了解一下音频播放的实现级别:离线播放:这里并不是指应用不联网,而是指播放本地音频文件,包括 ...

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

    引言 假如你现在打算做一个类似百度音乐.豆瓣电台的在线音乐类APP,你会怎样做? 首先了解一下音频播放的实现级别: (1) 离线播放:这里并不是指应用不联网,而是指播放本地音频文件,包括先下完完成音频 ...

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

    1.引言 假如你现在打算做一个类似百度音乐.豆瓣电台的在线音乐类APP,你会怎样做? 首先了解一下音频播放的实现级别:(1) 离线播放:这里并不是指应用不联网,而是指播放本地音频文件,包括先下完完成音 ...

  4. ios+html+音频播放,iOS音频篇:使用AVPlayer播放网络音乐

    2018-11-13更新:已更新工程配置和修改部分代码,Xcode9能直接运行此项目了.但由于项目中使用的豆瓣API已经停止支持,所以项目已不能正常演示,是否会继续更新就看缘分嘞 _... 引言 假如 ...

  5. iOS 音频的实时录制和播放

    需求:最近公司需要做一个楼宇对讲的功能:门口机(连接WIFI)拨号对室内机(对应的WIFI)的设备进行呼叫,室内机收到呼叫之后将对收到的数据进行UDP广播的转发,手机(连接对应的WIFI)收到视频流之 ...

  6. 基于hbuilder开发的微信小程序音乐播放系统,可添加本地音乐(含源码)

    可用hbuilder开发工具直接打开微信小程序,可直接运行,不需要调试,可直接使用.播放设置.下一首.上一首.暂停按键.播放列表等功能! 安装教学视频: 请点击!!! 源码下载地址:请点击>&g ...

  7. 【python】本地音乐播放器(PyQt5界面版)

    大家好,近期又学习了新内容,所以迫不及待想分享出来. 关于python Gui编写的界面版的音乐播放器,能实现本地音乐的播放. 目录 前期准备 设计界面 功能需求 播放.暂停 播放模式 时长显示 主窗 ...

  8. 【音乐播放】基于matlab GUI动感音乐播放【含Matlab源码 778期】

    ⛄一.获取代码方式 获取代码方式1: 完整代码已上传我的资源:[音乐播放]基于matlab GUI动感音乐播放[含Matlab源码 778期] 点击上面蓝色字体,直接付费下载,即可. 获取代码方式2: ...

  9. iOS 音频录制、播放(本地、网络)

    文章目录一.录音机(AVAudioRecorder)1.简介2.如何使用3.具体实现(开始.暂停.停止.播放 四个功能)4.附件实现demo二.播放音频1.播放本地音频文件(AVAudioPlayer ...

最新文章

  1. linux mate桌面主题下载_MATE-Desktop 1.11 发布下载,Linux 桌面
  2. scala入门之识别函数式风格
  3. 6种Python字符串反转方式
  4. 关于眼界、眼光、眼前的哪些....
  5. typescript的数据类型
  6. oracle长连接超时设置
  7. Codeforces Round #725 (Div. 3) 题解
  8. 人脸年龄编辑:无可奈何花落去,似曾相似春又来!
  9. Sqoop2开启Kerberos安全模式
  10. 资源放送丨《Oracle数据库索引分裂详解》PPT视频
  11. 【同行说技术】Java程序员小白变大神必读资料汇总(三)
  12. 计算机怎么调整显示英语翻译,翻译词汇:计算机显示英语词汇 口译词汇
  13. eclipse project修改 output folder
  14. MySQL 中while loop repeat 的基本用法
  15. 向量的数量积,向量积,混合积
  16. 校园网显示dns服务器解析出错,天翼校园网dns解析出错怎么办
  17. word中一个表格拆分成两个单独的表格
  18. linux qt fscanf,fscanf QT小部件C++
  19. C++获取打印系统当前时间、日期
  20. 达人评测 i5 12500h和锐龙r5 5600h选哪个好

热门文章

  1. 每周一品 · 无线充电设备中的磁性材料
  2. s11 day103 luffy项目结算部分+认证+django-redis
  3. 用墨刀设计原型,易被忽略的8种玩法。
  4. 汪延谈王志东离职问题 (转)
  5. 属鸡适不适合学计算机,生肖属相鸡适宜什么职业(详解)
  6. 通过ethtool命令解决网络的卡顿、时延、断断续续、路由带*****识别错误
  7. 动手学Docker-第二弹-基本操作
  8. linux服务器杀毒软件命令,悬镜Linux服务器卫士-木马查杀详解
  9. 随机地图生成--自己的一次尝试
  10. 手机开机启动慢是什么原因_手机开机慢,详细教您手机开机慢怎么办