原文出处 :http://msching.github.io/blog/2014/07/09/audio-in-ios-3/
前言
本来说好是要在第三篇中讲AudioFileStream
和AudioQueue
,但写着写着发现光AudioFileStream
就好多内容。最后还是决定分篇介绍,这篇先来说一下AudioFileStream
,下一篇计划说一下和AudioFileStream
类似的AudioFile
。下下篇再来说AudioQueue
。
在本篇那种将会提到计算音频时长duration和音频seek的方法,这些方法对于CBR编码形式的音频文件能够做到比較精确而对于VBR编码形式的会存在较大的误差(关于CBR和VBR,请看本系列的第一篇),详细讲到duration和seek时会再进行说明。
AudioFileStream介绍
在第一篇中说到AudioFileStreamer
时提到它的作用是用来读取採样率、码率、时长等基本信息以及分离音频帧。那么在官方文档中Apple是这样描写叙述的:
To play streamed audio content, such as from a network connection, use Audio File Stream
Services in concert with Audio Queue Services. Audio File Stream Services parses audio packets and metadata from common audio file container formats in a network bitstream. You can also use it to parse packets and metadata from on-disk files
依据Apple的描写叙述AudioFileStreamer
用在流播放中,当然不仅限于网络流,本地文件相同能够用它来读取信息和分离音频帧。
AudioFileStreamer
的主要数据是文件数据而不是文件路径,所以数据的读取须要使用者自行实现。
支持的文件格式有:
- MPEG-1 Audio Layer 3, used for .mp3 files
- MPEG-2 ADTS, used for the .aac audio data format
- AIFC
- AIFF
- CAF
- MPEG-4, used for .m4a, .mp4, and .3gp files
- NeXT
- WAVE
上述格式是iOS、MacOSX所支持的音频格式,这类格式能够被系统提供的API解码,假设想要解码其它的音频格式(如OGG、APE、FLAC)就须要自己实现解码器了。
初始化AudioFileStream
第一步,自然是要生成一个AudioFileStream
的实例:
1
2
3
4
5
|
|
第一个參数和之前的AudioSession的初始化方法一样是一个上下文对象。
第二个參数AudioFileStream_PropertyListenerProc
是歌曲信息解析的回调,每解析出一个歌曲信息都会进行一次回调;
第三个參数AudioFileStream_PacketsProc
是分离帧的回调,每解析出一部分帧就会进行一次回调;
第四个參数AudioFileTypeID
是文件类型的提示,这个參数来帮助AudioFileStream
对文件格式进行解析。
这个參数在文件信息不完整(比如信息有缺陷)时尤事实上用,它能够给与AudioFileStream
一定的提示,帮助其绕过文件里的错误或者缺失从而成功解析文件。所以在确定文件类型的情况下建议各位还是填上这个參数。假设无法确定能够传入0(原理上应该和这篇博文近似);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
|
第五个參数是返回的AudioFileStream实例相应的AudioFileStreamID
,这个ID须要保存起来作为兴许一些方法的參数使用。
返回值用来推断是否成功初始化(OSStatus == noErr)。
解析数据
在初始化完毕之后,仅仅要拿到文件数据就能够进行解析了。解析时调用方法:
1
2
3
4
|
|
第一个參数AudioFileStreamID
。即初始化时返回的ID;
第二个參数inDataByteSize,本次解析的数据长度;
第三个參数inData,本次解析的数据。
第四个參数是说本次的解析和上一次解析是否是连续的关系。假设是连续的传入0。否则传入kAudioFileStreamParseFlag_Discontinuity
。
这里须要插入解释一下何谓“连续”。在第一篇中我们提到过形如MP3的数据都以帧的形式存在的,解析时也须要以帧为单位解析。但在解码之前我们不可能知道每一个帧的边界在第几个字节,所以就会出现这种情况:我们传给AudioFileStreamParseBytes的数据在解析完毕之后会有一部分数据余下来。这部分数据是接下去那一帧的前半部分,假设再次有数据输入须要继续解析时就必须要用到前一次解析余下来的数据才干保证帧数据完整,所以在正常播放的情况下传入0就可以。眼下知道的须要传入kAudioFileStreamParseFlag_Discontinuity
的情况有两个,一个是在seek完毕之后显然seek后的数据和之前的数据全然无关。还有一个是开源播放器AudioStreamer的作者@Matt
Gallagher曾在自己的blog中提到过的:
the Audio File Stream Services hit me with a nasty bug: AudioFileStreamParseBytes will
crash when trying to parse a streaming MP3.
In this case, if we pass the kAudioFileStreamParseFlag_Discontinuity flag to AudioFileStreamParseBytes
on every invocation between receiving kAudioFileStreamProperty_ReadyToProducePackets and the first successful call to MyPacketsProc, then AudioFileStreamParseBytes will be extra cautious in its approach and won't crash.
Matt公布这篇blog是在2008年,这个Bug年代相当久远了,并且原因未知,到底是否修复也不得而知。并且因为环境不同(比方測试用的mp3文件和所处的iOS系统)无法重现这个问题,所以我个人认为还是依照Matt的work around在回调得到kAudioFileStreamProperty_ReadyToProducePackets
之后,在正常解析第一帧之前都传入kAudioFileStreamParseFlag_Discontinuity
比較好。
回到之前的内容,AudioFileStreamParseBytes
方法的返回值表示当前的数据是否被正常解析。假设OSStatus的值不是noErr则表示解析不成功,当中错误码包含:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
|
大多数都能够从字面上理解,须要提一下的是kAudioFileStreamError_NotOptimized
,文档上是这么说的:
It is not possible to produce output packets because the file's packet table or other
defining info is either not present or is after the audio data.
它的含义是说这个音频文件的文件头不存在或者说文件头可能在文件的末尾,当前无法正常Parse。换句话说就是这个文件须要所有下载完才干播放,无法流播。
注意AudioFileStreamParseBytes
方法每一次调用都应该注意返回值。一旦出现错误就能够不必继续Parse了。
解析文件格式信息
在调用AudioFileStreamParseBytes
方法进行解析时会首先读取格式信息。并同步的进入AudioFileStream_PropertyListenerProc
回调方法
来看一下这个回调方法的定义
1
2
3
4
|
|
回调的第一个參数是Open方法中的上下文对象;
第二个參数inAudioFileStream是和Open方法中第四个返回參数AudioFileStreamID
一样。表示当前FileStream的ID。
第三个參数是此次回调解析的信息ID。表示当前PropertyID相应的信息已经解析完毕信息(比如数据格式、音频数据的偏移量等等),使用者能够通过AudioFileStreamGetProperty
接口获取PropertyID相应的值或者数据结构。
1
2
3
4
|
|
第四个參数ioFlags是一个返回參数,表示这个property是否须要被缓存,假设须要赋值kAudioFileStreamPropertyFlag_PropertyIsCached
否则不赋值(这个參数我也不知道应该在啥场景下使用。。一直都没去理他);
这个回调会进来多次。但并非每一次都须要进行处理,能够依据需求处理须要的PropertyID进行处理(PropertyID列表例如以下)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
|
这里列几个我觉得比較重要的PropertyID:
1、kAudioFileStreamProperty_BitRate
:
表示音频数据的码率,获取这个Property是为了计算音频的总时长Duration(由于AudioFileStream没有这种接口。
。)。
1
2
3
4
5
6
7
|
|
2014.8.2 补充: 发如今流播放的情况下。有时数据流量比較小时会出现ReadyToProducePackets
还是没有获取到bitRate的情况。这时就须要分离一些拼音帧然后计算平均bitRate,计算公式例如以下:
1
|
|
2、kAudioFileStreamProperty_DataOffset
:
表示音频数据在整个音频文件里的offset(由于大多数音频文件都会有一个文件头之后才使真正的音频数据),这个值在seek时会发挥比較大的作用。音频的seek并非直接seek文件位置而seek时间(比方seek到2分10秒的位置),seek时会依据时间计算出音频数据的字节offset然后须要再加上音频数据的offset才干得到在文件里的真正offset。
1
2
3
4
5
6
7
|
|
3、kAudioFileStreamProperty_DataFormat
表示音频文件结构信息,是一个AudioStreamBasicDescription的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
|
4、kAudioFileStreamProperty_FormatList
作用和kAudioFileStreamProperty_DataFormat
是一样的,差别在于用这个PropertyID获取到是一个AudioStreamBasicDescription的数组,这个參数是用来支持AAC
SBR这种包括多个文件类型的音频格式。
因为究竟有多少个format我们并不知晓,所以须要先获取一下总数据大小:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
|
5、kAudioFileStreamProperty_AudioDataByteCount
顾名思义,音频文件里音频数据的总量。
这个Property的作用一是用来计算音频的总时长,二是能够在seek时用来计算时间相应的字节offset。
1
2
3
4
5
6
7
|
|
2014.8.2 补充: 发如今流播放的情况下,有时数据流量比較小时会出现ReadyToProducePackets
还是没有获取到audioDataByteCount的情况,这时就须要近似计算audioDataByteCount。一般来说音频文件的总大小一定是能够得到的(利用文件系统或者Http请求中的contentLength),那么计算方法例如以下:
1
2
3
|
|
5、kAudioFileStreamProperty_ReadyToProducePackets
这个PropertyID能够不必获取相应的值,一旦回调中这个PropertyID出现就代表解析完毕,接下来能够对音频数据进行帧分离了。
计算时长Duration
获取时长的最佳方法是从ID3信息中去读取,那样是最准确的。
假设ID3信息中没有存,那就依赖于文件头中的信息去计算了。
计算duration的公式例如以下:
1
|
|
音频数据的字节总量audioDataByteCount能够通过kAudioFileStreamProperty_AudioDataByteCount
获取,码率bitRate能够通过kAudioFileStreamProperty_BitRate
获取也能够通过Parse一部分数据后计算平均码率来得到。
对于CBR数据来说用这种计算方法的duration会比較准确,对于VBR数据就不好说了。
所以对于VBR数据来说,最好是可以从ID3信息中获取到duration,获取不到再想办法通过计算平均码率的途径来计算duration。
分离音频帧
读取格式信息完毕之后继续调用AudioFileStreamParseBytes
方法能够对帧进行分离,并同步的进入AudioFileStream_PacketsProc
回调方法。
回调的定义:
1
2
3
4
5
|
|
第一个參数,一如既往的上下文对象;
第二个參数,本次处理的数据大小;
第三个參数。本次总共处理了多少帧(即代码里的Packet)。
第四个參数。本次处理的全部数据。
第五个參数,AudioStreamPacketDescription
数组。存储了每一帧数据是从第几个字节開始的。这一帧总共多少字节。
1
2
3
4
5
6
7
8
|
|
以下是我依照自己的理解实现的回调方法片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
|
inPacketDescriptions这个字段为空时须要按CBR的数据处理。但事实上在解析CBR数据时inPacketDescriptions一般也会有返回,由于即使是CBR数据帧的大小也不是恒定不变的,比如CBR的MP3会在每一帧的数据后放1 byte的填充位。这个填充位也并不是时时刻刻存在,所以帧的大小会有1 byte的浮动。
(比方採样率44.1KHZ。码率160kbps的CBR MP3文件每一帧的大小在522字节和523字节浮动。所以不能由于有inPacketDescriptions没有返回NULL而判定音频数据就是VBR编码的)。
Seek
就音频的角度来seek功能描写叙述为“我要拖到xx分xx秒”,而实际操作时我们须要操作的是文件,所以我们须要知道的是“我要拖到xx分xx秒”这个操作相应到文件上是要从第几个字节開始读取音频数据。
对于原始的PCM数据来说每个PCM帧都是固定长度的,相应的播放时长也是固定的,但一旦转换成压缩后的音频数据就会由于编码形式的不同而不同了。
对于CBR而言每个帧中所包括的PCM数据帧是恒定的。所以每一帧相应的播放时长也是恒定的;而VBR则不同。为了保证数据最优而且文件大小最小,VBR的每一帧中所包括的PCM数据帧是不固定的。这就导致在流播放的情况下VBR的数据想要做seek并不easy。
这里我们也仅仅讨论CBR下的seek。
CBR数据的seek通常是这样实现的(參考并改动自matt的blog):
1、近似地计算应该seek到哪个字节
1
2
3
4
5
6
7
|
|
2、计算seekToTime相应的是第几个帧(Packet)
我们能够利用之前Parse得到的音频格式信息来计算PacketDuration。
audioItem.fileFormat.mFramesPerPacket /audioItem.fileFormat.mSampleRate;
1
2
3
4
5
6
|
|
3、使用AudioFileStreamSeek
计算精确的字节偏移和时间
AudioFileStreamSeek
能够用来寻找某一个帧(Packet)相应的字节偏移(byte offset):
- 假设找到了就会把ioFlags加上kAudioFileStreamSeekFlag_OffsetIsEstimated,而且给outDataByteOffset赋值。outDataByteOffset就是输入的seekToPacket相应的字节偏移量,我们能够依据outDataByteOffset来计算出精确的seekOffset和seekToTime。
- 假设没找到那么还是应该用第1步计算出来的approximateSeekOffset来做seek;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
|
4、依照seekByteOffset读取相应的数据继续使用AudioFileStreamParseByte
进行解析
假设是网络流能够通过设置range头来获取字节,本地文件的话直接seek就好了。
调用AudioFileStreamParseByte
时注意刚seek完第一次Parse数据须要加參数kAudioFileStreamParseFlag_Discontinuity
。
关闭AudioFileStream
AudioFileStream
使用完成后须要调用AudioFileStreamClose
进行关闭。没啥特别须要注意的。
1
|
|
小结
本篇关于AudioFileStream
做了具体介绍。小结一下:
-
使用
AudioFileStream
首先须要调用AudioFileStreamOpen
,须要注意的是尽量提供inFileTypeHint參数帮助AudioFileStream
解析数据,调用完毕后记录AudioFileStreamID
。 -
当有数据时调用
AudioFileStreamParseBytes
进行解析,每一次解析都须要注意返回值,返回值一旦出现noErr以外的值就代表Parse出错,当中kAudioFileStreamError_NotOptimized
代表该文件缺少头信息或者其头信息在文件尾部不适合流播放; -
使用
AudioFileStreamParseBytes
须要注意第四个參数在须要合适的时候传入kAudioFileStreamParseFlag_Discontinuity
; -
调用
AudioFileStreamParseBytes
后会首先同步进入AudioFileStream_PropertyListenerProc
回调来解析文件格式信息。假设回调得到kAudioFileStreamProperty_ReadyToProducePackets
表示解析格式信息完毕。 -
解析格式信息完毕后继续调用
AudioFileStreamParseBytes
会进入MyAudioFileStreamPacketsCallBack
回调来分离音频帧。在回调中应该将分离出来的帧信息保存到自己的buffer中 -
seek时须要先近似的计算seekTime相应的seekByteOffset。然后利用
AudioFileStreamSeek
计算精确的offset,假设能得到精确的offset就修正一下seektime。假设无法得到精确的offset就用之前的近似结果 -
AudioFileStream
使用完成后须要调用AudioFileStreamClose
进行关闭;
演示样例代码
AudioStreamer和FreeStreamer这两个优秀的开源播放器都用到AudioFileStream
大家能够借鉴。我自己也写了一个简单的AudioFileStream封装。
下篇预告
下一篇将讲述怎样使用AudioFile
。